From 5ba885dbc4f24781d6a3240ddfc0c03177b12f1e Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 21 Apr 2020 22:20:36 -0700 Subject: [PATCH] Take input to `imdl torrent create` as positional Allow taking the `--input` argument to `imdl torrent create` as a positional argument, so the following now works: imdl torrent create foo Taking input by flag `--input` still works. type: changed fixes: - https://github.com/casey/intermodal/issues/375 --- CHANGELOG.md | 5 +- book/src/notes.md | 1 - src/common.rs | 3 + src/main.rs | 1 + src/subcommand/torrent/create.rs | 160 ++++++++++++++++-- .../torrent/create/create_content.rs | 4 +- src/xor_args.rs | 17 ++ 7 files changed, 169 insertions(+), 22 deletions(-) delete mode 100644 book/src/notes.md create mode 100644 src/xor_args.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8eed9..f9d0f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ Changelog ========= -UNRELEASED - 2020-04-21 +UNRELEASED - 2020-04-22 ----------------------- -- :wrench: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Don't commit man pages - _Casey Rodarmor _ +- :zap: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Take input to `imdl torrent create` as positional - Fixes [#375](https://github.com/casey/intermodal/issues/375) - _Casey Rodarmor _ +- :wrench: [`c22df5a08326`](https://github.com/casey/intermodal/commit/c22df5a083265b03abd5531b1f5b2aad60aa68cd) Don't commit man pages - _Casey Rodarmor _ - :wrench: [`4d67d3a10d17`](https://github.com/casey/intermodal/commit/4d67d3a10d17db3c63af092a936eb5994ee107b1) Don't commit the book - _Casey Rodarmor _ - :wrench: [`28114c3d64dd`](https://github.com/casey/intermodal/commit/28114c3d64dde5e0275c936b0019eaf4760ba559) Don't commit shell completion scripts - _Casey Rodarmor _ - :art: [`4f4464e3a2a7`](https://github.com/casey/intermodal/commit/4f4464e3a2a7f4aaffea8dbe38dd110ad9be4393) Get `st_flags` from `MetadataExt` on MacOS - _Casey Rodarmor _ diff --git a/book/src/notes.md b/book/src/notes.md deleted file mode 100644 index 17e0f0d..0000000 --- a/book/src/notes.md +++ /dev/null @@ -1 +0,0 @@ -# Notes diff --git a/src/common.rs b/src/common.rs index 1a757c5..e6f162b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -53,6 +53,9 @@ pub(crate) use log::trace; // modules pub(crate) use crate::{consts, error, host_port_parse_error}; +// functions +pub(crate) use crate::xor_args::xor_args; + // traits pub(crate) use crate::{ input_stream::InputStream, into_u64::IntoU64, into_usize::IntoUsize, invariant::Invariant, diff --git a/src/main.rs b/src/main.rs index ec1529b..46d1052 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,6 +100,7 @@ mod torrent_summary; mod use_color; mod verifier; mod walker; +mod xor_args; fn main() { if let Err(code) = Env::main().status() { diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index e981c53..379dafc 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -5,6 +5,16 @@ use create_step::CreateStep; mod create_content; mod create_step; +const INPUT_HELP: &str = "Read torrent contents from `PATH`. If `PATH` is a file, torrent will be \ + a single-file torrent. If `PATH` is a directory, torrent will be a \ + multi-file torrent. If `PATH` is `-`, read from standard input. Piece \ + length defaults to 256KiB when reading from standard input if \ + `--piece-length` is not given."; + +const INPUT_FLAG: &str = "input-flag"; + +const INPUT_POSITIONAL: &str = ""; + #[derive(StructOpt)] #[structopt( help_message(consts::HELP_MESSAGE), @@ -115,17 +125,25 @@ Examples: )] include_junk: bool, #[structopt( + name = INPUT_POSITIONAL, + value_name = "INPUT", + empty_values = false, + required_unless = INPUT_FLAG, + conflicts_with = INPUT_FLAG, + parse(try_from_os_str = InputTarget::try_from_os_str), + help = INPUT_HELP, + )] + input_positional: Option, + #[structopt( + name = INPUT_FLAG, long = "input", short = "i", value_name = "PATH", - empty_values(false), + empty_values = false, parse(try_from_os_str = InputTarget::try_from_os_str), - help = "Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file \ - torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` \ - is `-`, read from standard input. Piece length defaults to 256KiB when reading from \ - standard input if `--piece-length` is not given.", + help = INPUT_HELP, )] - input: InputTarget, + input_flag: Option, #[structopt( long = "link", help = "Print created torrent `magnet:` URL to standard output" @@ -144,7 +162,8 @@ Examples: value_name = "TEXT", help = "Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. \ Required when `--input -`.", - required_if("input", "-") + required_if(INPUT_FLAG, "-"), + required_if(INPUT_POSITIONAL, "-") )] name: Option, #[structopt( @@ -192,7 +211,8 @@ Sort in ascending order by size, break ties in descending path order: value_name = "TARGET", empty_values(false), parse(try_from_os_str = OutputTarget::try_from_os_str), - required_if("input", "-"), + required_if(INPUT_FLAG, "-"), + required_if(INPUT_POSITIONAL, "-"), help = "Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. \ Defaults to the argument to `--input` with an `.torrent` extension appended. Required \ when `--input -`.", @@ -250,6 +270,13 @@ Sort in ascending order by size, break ties in descending path order: impl Create { pub(crate) fn run(self, env: &mut Env, options: &Options) -> Result<(), Error> { + let input = xor_args( + "input_positional", + &self.input_positional, + "input_flag", + &self.input_flag, + )?; + let mut linter = Linter::new(); linter.allow(self.allowed_lints.iter().cloned()); @@ -281,10 +308,10 @@ impl Create { }; if !options.quiet { - CreateStep::Searching { input: &self.input }.print(env)?; + CreateStep::Searching { input: &input }.print(env)?; } - let content = CreateContent::from_create(&self, env)?; + let content = CreateContent::from_create(&self, &input, env)?; let output = content.output.resolve(env)?; @@ -398,7 +425,7 @@ impl Create { #[cfg(test)] { - if let InputTarget::Path(path) = &self.input { + if let InputTarget::Path(path) = &input { let deserialized = bendy::serde::de::from_bytes::(&bytes).unwrap(); assert_eq!(deserialized, metainfo); @@ -451,6 +478,79 @@ mod tests { assert!(matches!(env.run(), Err(Error::Clap { .. }))); } + #[test] + fn input_arguments_conflict() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "bar", + ], + tree: {}, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn require_name_if_input_flag_stdin() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "-", + ], + tree: {}, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn require_name_if_input_positional_stdin() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "-", + ], + tree: {}, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn require_output_if_input_flag_stdin() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "-", + "--name", + "foo", + ], + tree: {}, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn require_output_if_input_positional_stdin() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "-", + "--name", + "foo", + ], + tree: {}, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + #[test] fn require_input_present() { let mut env = test_env! { @@ -507,6 +607,30 @@ mod tests { Ok(()) } + #[test] + fn input_positional() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "foo", + ], + tree: { + foo: { + bar: "", + baz: "", + }, + } + }; + env.assert_ok(); + let metainfo = env.load_metainfo("foo.torrent"); + assert_eq!(metainfo.info.name, "foo"); + assert_matches!( + metainfo.info.mode, + Mode::Multiple{files} if files.len() == 2 + ); + } + #[test] fn input_dot() { let mut env = test_env! { @@ -526,10 +650,12 @@ mod tests { } }; env.assert_ok(); - // let metainfo = env.load_metainfo("../dir.torrent"); - // assert_eq!(metainfo.info.name, "dir"); - // assert_matches!(metainfo.info.mode, Mode::Multiple{files} if files.len() - // == 1); + let metainfo = env.load_metainfo("../dir.torrent"); + assert_eq!(metainfo.info.name, "dir"); + assert_matches!( + metainfo.info.mode, + Mode::Multiple{files} if files.len() == 1 + ); } #[test] @@ -2700,7 +2826,7 @@ Content Size 9 bytes ], tree: {}, }; - assert!(matches!(env.run(), Err(Error::Clap { .. }))); + assert_matches!(env.run(), Err(Error::Clap { .. })); } #[test] @@ -2718,7 +2844,7 @@ Content Size 9 bytes ], tree: {}, }; - assert!(matches!(env.run(), Err(Error::Clap { .. }))); + assert_matches!(env.run(), Err(Error::Clap { .. })); } #[test] diff --git a/src/subcommand/torrent/create/create_content.rs b/src/subcommand/torrent/create/create_content.rs index 00c0d5f..e04fefc 100644 --- a/src/subcommand/torrent/create/create_content.rs +++ b/src/subcommand/torrent/create/create_content.rs @@ -11,8 +11,8 @@ pub(crate) struct CreateContent { } impl CreateContent { - pub(crate) fn from_create(create: &Create, env: &mut Env) -> Result { - match &create.input { + pub(crate) fn from_create(create: &Create, input: &InputTarget, env: &mut Env) -> Result { + match input { InputTarget::Path(path) => { let spinner = if env.err().is_styled_term() { let style = ProgressStyle::default_spinner() diff --git a/src/xor_args.rs b/src/xor_args.rs new file mode 100644 index 0000000..b773feb --- /dev/null +++ b/src/xor_args.rs @@ -0,0 +1,17 @@ +use crate::common::*; + +pub(crate) fn xor_args( + a_name: &str, + a: &Option, + b_name: &str, + b: &Option, +) -> Result { + let target = a.as_ref().xor(b.as_ref()).ok_or_else(|| { + Error::internal(format!( + "Expected exactly one of the arguments `{}` or `{}` to be set", + a_name, b_name + )) + })?; + + Ok(target.clone()) +}