Procházet zdrojové kódy

Add command to generate completions

timvisee před 6 roky
rodič
revize
2fe8e24fb1

+ 1 - 1
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"

+ 3 - 1
README.md

@@ -539,7 +539,7 @@ documentation [here][send-encryption].
 $ ffsend help
 
 ffsend 0.2.36
-Tim Visee <timvisee.com>
+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 <VERSION>                 Server API version to use, '-' to lookup [env: FFSEND_API]
     -H, --history <FILE>                Use the specified history file [env: FFSEND_HISTORY]
     -t, --timeout <SECONDS>             Request timeout (0 to disable) [env: FFSEND_TIMEOUT]
     -T, --transfer-timeout <SECONDS>    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]

+ 61 - 0
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),
+}

+ 34 - 0
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!()
+    }
+}

+ 1 - 0
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;

+ 10 - 4
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> {
+        GenerateMatcher::with(&self.matches)
+    }
+
     /// Get the history sub command, if matched.
     #[cfg(feature = "history")]
     pub fn history(&'a self) -> Option<HistoryMatcher> {

+ 64 - 0
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<Shell> {
+        // 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<Self> {
+        matches
+            .subcommand_matches("generate")?
+            .subcommand_matches("completions")
+            .map(|matches| CompletionsMatcher { matches })
+    }
+}

+ 29 - 0
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> {
+        CompletionsMatcher::with(&self.root)
+    }
+}
+
+impl<'a> Matcher<'a> for GenerateMatcher<'a> {
+    fn with(root: &'a ArgMatches) -> Option<Self> {
+        root.subcommand_matches("generate")
+            .map(|matches| GenerateMatcher {
+                root,
+                _matches: matches,
+            })
+    }
+}

+ 2 - 0
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;

+ 1 - 1
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

+ 33 - 0
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"),
+            )
+    }
+}

+ 18 - 0
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())
+    }
+}

+ 2 - 0
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;

+ 11 - 0
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<ExistsError> for ActionError {
     }
 }
 
+impl From<CliGenerateCompletionsError> for ActionError {
+    fn from(err: CliGenerateCompletionsError) -> ActionError {
+        ActionError::GenerateCompletions(err)
+    }
+}
+
 #[cfg(feature = "history")]
 impl From<CliHistoryError> for ActionError {
     fn from(err: CliHistoryError) -> ActionError {

+ 8 - 0
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")]
     {