From 2fe8e24fb1d00645221fc4c750250d4da454f3a1 Mon Sep 17 00:00:00 2001 From: timvisee Date: Mon, 18 Mar 2019 16:31:43 +0100 Subject: [PATCH] Add command to generate completions --- Cargo.toml | 2 +- README.md | 4 +- src/action/generate/completions.rs | 61 +++++++++++++++++++++++ src/action/generate/mod.rs | 34 +++++++++++++ src/action/mod.rs | 1 + src/cmd/handler.rs | 14 ++++-- src/cmd/matcher/generate/completions.rs | 64 +++++++++++++++++++++++++ src/cmd/matcher/generate/mod.rs | 29 +++++++++++ src/cmd/matcher/mod.rs | 2 + src/cmd/subcmd/download.rs | 2 +- src/cmd/subcmd/generate/completions.rs | 33 +++++++++++++ src/cmd/subcmd/generate/mod.rs | 18 +++++++ src/cmd/subcmd/mod.rs | 2 + src/error.rs | 11 +++++ src/main.rs | 8 ++++ 15 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 src/action/generate/completions.rs create mode 100644 src/action/generate/mod.rs create mode 100644 src/cmd/matcher/generate/completions.rs create mode 100644 src/cmd/matcher/generate/mod.rs create mode 100644 src/cmd/subcmd/generate/completions.rs create mode 100644 src/cmd/subcmd/generate/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 0903354..a9aa73a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ no-color = ["colored/no-color"] [dependencies] chbs = "0.0.8" chrono = "0.4" -clap = "2.31" +clap = "2.32" colored = "1.7" derive_builder = "0.7" directories = "1.0" diff --git a/README.md b/README.md index e809cef..e2e818d 100644 --- a/README.md +++ b/README.md @@ -539,7 +539,7 @@ documentation [here][send-encryption]. $ ffsend help ffsend 0.2.36 -Tim Visee +Tim Visee <3a4fb3964f@sinenomine.email> Easily and securely share files from the command line. A fully featured Firefox Send client. @@ -557,6 +557,7 @@ FLAGS: -y, --yes Assume yes for prompts OPTIONS: + -A, --api Server API version to use, '-' to lookup [env: FFSEND_API] -H, --history Use the specified history file [env: FFSEND_HISTORY] -t, --timeout Request timeout (0 to disable) [env: FFSEND_TIMEOUT] -T, --transfer-timeout Transfer timeout (0 to disable) [env: FFSEND_TRANSFER_TIMEOUT] @@ -567,6 +568,7 @@ SUBCOMMANDS: debug View debug information [aliases: dbg] delete Delete a shared file [aliases: del] exists Check whether a remote file exists [aliases: e] + generate Generate assets [aliases: gen] help Prints this message or the help of the given subcommand(s) history View file history [aliases: h] info Fetch info about a shared file [aliases: i] diff --git a/src/action/generate/completions.rs b/src/action/generate/completions.rs new file mode 100644 index 0000000..9e29b36 --- /dev/null +++ b/src/action/generate/completions.rs @@ -0,0 +1,61 @@ +use std::fs; +use std::io; + +use clap::ArgMatches; + +use crate::cmd::matcher::{generate::completions::CompletionsMatcher, main::MainMatcher, Matcher}; +use crate::error::ActionError; + +/// A file completions action. +pub struct Completions<'a> { + cmd_matches: &'a ArgMatches<'a>, +} + +impl<'a> Completions<'a> { + /// Construct a new completions action. + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { + Self { cmd_matches } + } + + /// Invoke the completions action. + // TODO: create a trait for this method + pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); + let matcher_completions = CompletionsMatcher::with(self.cmd_matches).unwrap(); + + // Obtian shells to generate completions for, build application definition + let shells = matcher_completions.shells(); + let dir = matcher_completions.output(); + let verbose = matcher_main.verbose(); + let mut app = crate::cmd::handler::Handler::build(); + + // If the directory does not exist yet, attempt to create it + if !dir.is_dir() { + fs::create_dir_all(&dir).map_err(Error::CreateOutputDir)?; + } + + // Generate completions + for shell in shells { + if verbose { + print!( + "Generating completions for {}...", + format!("{}", shell).to_lowercase() + ); + } + app.gen_completions(crate_name!(), shell, &dir); + if verbose { + println!(" done."); + } + } + + Ok(()) + } +} + +#[derive(Debug, Fail)] +pub enum Error { + /// An error occurred while creating the output directory. + #[fail(display = "failed to create output directory, it doesn't exist")] + CreateOutputDir(#[cause] io::Error), +} diff --git a/src/action/generate/mod.rs b/src/action/generate/mod.rs new file mode 100644 index 0000000..f754f08 --- /dev/null +++ b/src/action/generate/mod.rs @@ -0,0 +1,34 @@ +pub mod completions; + +use clap::ArgMatches; + +use crate::cmd::matcher::{generate::GenerateMatcher, Matcher}; +use crate::error::ActionError; +use completions::Completions; + +/// A file generate action. +pub struct Generate<'a> { + cmd_matches: &'a ArgMatches<'a>, +} + +impl<'a> Generate<'a> { + /// Construct a new generate action. + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { + Self { cmd_matches } + } + + /// Invoke the generate action. + // TODO: create a trait for this method + pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matcher + let matcher_generate = GenerateMatcher::with(self.cmd_matches).unwrap(); + + // Match shell completions + if matcher_generate.matcher_completions().is_some() { + return Completions::new(self.cmd_matches).invoke(); + } + + // Unreachable, clap will print help for missing sub command instead + unreachable!() + } +} diff --git a/src/action/mod.rs b/src/action/mod.rs index b84aa4b..aeecd57 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -2,6 +2,7 @@ pub mod debug; pub mod delete; pub mod download; pub mod exists; +pub mod generate; #[cfg(feature = "history")] pub mod history; pub mod info; diff --git a/src/cmd/handler.rs b/src/cmd/handler.rs index 0e84e0d..39876c3 100644 --- a/src/cmd/handler.rs +++ b/src/cmd/handler.rs @@ -9,14 +9,14 @@ use super::arg::{ArgApi, CmdArg}; #[cfg(feature = "history")] use super::matcher::HistoryMatcher; use super::matcher::{ - DebugMatcher, DeleteMatcher, DownloadMatcher, ExistsMatcher, InfoMatcher, Matcher, - ParamsMatcher, PasswordMatcher, UploadMatcher, VersionMatcher, + DebugMatcher, DeleteMatcher, DownloadMatcher, ExistsMatcher, GenerateMatcher, InfoMatcher, + Matcher, ParamsMatcher, PasswordMatcher, UploadMatcher, VersionMatcher, }; #[cfg(feature = "history")] use super::subcmd::CmdHistory; use super::subcmd::{ - CmdDebug, CmdDelete, CmdDownload, CmdExists, CmdInfo, CmdParams, CmdPassword, CmdUpload, - CmdVersion, + CmdDebug, CmdDelete, CmdDownload, CmdExists, CmdGenerate, CmdInfo, CmdParams, CmdPassword, + CmdUpload, CmdVersion, }; #[cfg(feature = "infer-command")] use crate::config::INFER_COMMANDS; @@ -153,6 +153,7 @@ impl<'a: 'b, 'b> Handler<'a> { .subcommand(CmdDelete::build()) .subcommand(CmdDownload::build().display_order(2)) .subcommand(CmdExists::build()) + .subcommand(CmdGenerate::build()) .subcommand(CmdInfo::build()) .subcommand(CmdParams::build()) .subcommand(CmdPassword::build()) @@ -257,6 +258,11 @@ impl<'a: 'b, 'b> Handler<'a> { ExistsMatcher::with(&self.matches) } + /// Get the generate sub command, if matched. + pub fn generate(&'a self) -> Option { + GenerateMatcher::with(&self.matches) + } + /// Get the history sub command, if matched. #[cfg(feature = "history")] pub fn history(&'a self) -> Option { diff --git a/src/cmd/matcher/generate/completions.rs b/src/cmd/matcher/generate/completions.rs new file mode 100644 index 0000000..51f87e0 --- /dev/null +++ b/src/cmd/matcher/generate/completions.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use clap::{ArgMatches, Shell}; + +use super::Matcher; + +/// The completions completions command matcher. +pub struct CompletionsMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> CompletionsMatcher<'a> { + /// Get the shells to generate completions for. + pub fn shells(&'a self) -> Vec { + // Get the raw list of shells + let raw = self + .matches + .values_of("SHELL") + .expect("no shells were given"); + + // Parse the list of shell names, deduplicate + let mut shells: Vec<_> = raw + .into_iter() + .map(|name| name.trim().to_lowercase()) + .map(|name| { + if name == "all" { + Shell::variants() + .iter() + .map(|name| name.to_string()) + .collect() + } else { + vec![name] + } + }) + .flatten() + .collect(); + shells.sort_unstable(); + shells.dedup(); + + // Parse the shell names + shells + .into_iter() + .map(|name| Shell::from_str(&name).expect("failed to parse shell name")) + .collect() + } + + /// The target directory to output the shell completion files to. + pub fn output(&'a self) -> PathBuf { + self.matches + .value_of("output") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("./")) + } +} + +impl<'a> Matcher<'a> for CompletionsMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches + .subcommand_matches("generate")? + .subcommand_matches("completions") + .map(|matches| CompletionsMatcher { matches }) + } +} diff --git a/src/cmd/matcher/generate/mod.rs b/src/cmd/matcher/generate/mod.rs new file mode 100644 index 0000000..c707a12 --- /dev/null +++ b/src/cmd/matcher/generate/mod.rs @@ -0,0 +1,29 @@ +pub mod completions; + +use clap::ArgMatches; + +use super::Matcher; +use completions::CompletionsMatcher; + +/// The generate command matcher. +pub struct GenerateMatcher<'a> { + root: &'a ArgMatches<'a>, + _matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> GenerateMatcher<'a> { + /// Get the generate completions sub command, if matched. + pub fn matcher_completions(&'a self) -> Option { + CompletionsMatcher::with(&self.root) + } +} + +impl<'a> Matcher<'a> for GenerateMatcher<'a> { + fn with(root: &'a ArgMatches) -> Option { + root.subcommand_matches("generate") + .map(|matches| GenerateMatcher { + root, + _matches: matches, + }) + } +} diff --git a/src/cmd/matcher/mod.rs b/src/cmd/matcher/mod.rs index 6bf7e33..5f3b805 100644 --- a/src/cmd/matcher/mod.rs +++ b/src/cmd/matcher/mod.rs @@ -2,6 +2,7 @@ pub mod debug; pub mod delete; pub mod download; pub mod exists; +pub mod generate; #[cfg(feature = "history")] pub mod history; pub mod info; @@ -16,6 +17,7 @@ pub use self::debug::DebugMatcher; pub use self::delete::DeleteMatcher; pub use self::download::DownloadMatcher; pub use self::exists::ExistsMatcher; +pub use self::generate::GenerateMatcher; #[cfg(feature = "history")] pub use self::history::HistoryMatcher; pub use self::info::InfoMatcher; diff --git a/src/cmd/subcmd/download.rs b/src/cmd/subcmd/download.rs index dfdd7cb..5614016 100644 --- a/src/cmd/subcmd/download.rs +++ b/src/cmd/subcmd/download.rs @@ -23,7 +23,7 @@ impl CmdDownload { .alias("out") .alias("file") .value_name("PATH") - .help("The output file or directory"), + .help("Output file or directory"), ); // Optional archive support diff --git a/src/cmd/subcmd/generate/completions.rs b/src/cmd/subcmd/generate/completions.rs new file mode 100644 index 0000000..ffebe98 --- /dev/null +++ b/src/cmd/subcmd/generate/completions.rs @@ -0,0 +1,33 @@ +use clap::{App, Arg, Shell, SubCommand}; + +/// The generate completions command definition. +pub struct CmdCompletions; + +impl CmdCompletions { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("completions") + .about("Shell completions") + .alias("completion") + .alias("complete") + .arg( + Arg::with_name("SHELL") + .help("Shell type to generate completions for") + .required(true) + .multiple(true) + .takes_value(true) + .possible_value("all") + .possible_values(&Shell::variants()) + .case_insensitive(true), + ) + .arg( + Arg::with_name("output") + .long("output") + .short("o") + .alias("output-dir") + .alias("out") + .alias("dir") + .value_name("DIR") + .help("Shell completion files output directory"), + ) + } +} diff --git a/src/cmd/subcmd/generate/mod.rs b/src/cmd/subcmd/generate/mod.rs new file mode 100644 index 0000000..5c46105 --- /dev/null +++ b/src/cmd/subcmd/generate/mod.rs @@ -0,0 +1,18 @@ +pub mod completions; + +use clap::{App, AppSettings, SubCommand}; + +use completions::CmdCompletions; + +/// The generate command definition. +pub struct CmdGenerate; + +impl CmdGenerate { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("generate") + .about("Generate assets") + .visible_alias("gen") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand(CmdCompletions::build()) + } +} diff --git a/src/cmd/subcmd/mod.rs b/src/cmd/subcmd/mod.rs index 7715e8e..eacdbfe 100644 --- a/src/cmd/subcmd/mod.rs +++ b/src/cmd/subcmd/mod.rs @@ -2,6 +2,7 @@ pub mod debug; pub mod delete; pub mod download; pub mod exists; +pub mod generate; #[cfg(feature = "history")] pub mod history; pub mod info; @@ -15,6 +16,7 @@ pub use self::debug::CmdDebug; pub use self::delete::CmdDelete; pub use self::download::CmdDownload; pub use self::exists::CmdExists; +pub use self::generate::CmdGenerate; #[cfg(feature = "history")] pub use self::history::CmdHistory; pub use self::info::CmdInfo; diff --git a/src/error.rs b/src/error.rs index ec35687..4d8668a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ use ffsend_api::action::version::Error as VersionError; use ffsend_api::file::remote_file::FileParseError; use crate::action::download::Error as CliDownloadError; +use crate::action::generate::completions::Error as CliGenerateCompletionsError; #[cfg(feature = "history")] use crate::action::history::Error as CliHistoryError; use crate::action::info::Error as CliInfoError; @@ -56,6 +57,10 @@ pub enum ActionError { #[fail(display = "failed to check whether the file exists")] Exists(#[cause] ExistsError), + /// An error occurred while generating completions. + #[fail(display = "failed to generate shell completions")] + GenerateCompletions(#[cause] CliGenerateCompletionsError), + /// An error occurred while processing the file history. #[cfg(feature = "history")] #[fail(display = "failed to process the history")] @@ -99,6 +104,12 @@ impl From for ActionError { } } +impl From for ActionError { + fn from(err: CliGenerateCompletionsError) -> ActionError { + ActionError::GenerateCompletions(err) + } +} + #[cfg(feature = "history")] impl From for ActionError { fn from(err: CliHistoryError) -> ActionError { diff --git a/src/main.rs b/src/main.rs index c237e5a..1b1d400 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ use crate::action::debug::Debug; use crate::action::delete::Delete; use crate::action::download::Download; use crate::action::exists::Exists; +use crate::action::generate::Generate; #[cfg(feature = "history")] use crate::action::history::History; use crate::action::info::Info; @@ -92,6 +93,13 @@ fn invoke_action(handler: &Handler) -> Result<(), Error> { .map_err(|err| err.into()); } + // Match the generate command + if handler.generate().is_some() { + return Generate::new(handler.matches()) + .invoke() + .map_err(|err| err.into()); + } + // Match the history command #[cfg(feature = "history")] {