Add command to generate completions

This commit is contained in:
timvisee 2019-03-18 16:31:43 +01:00
parent d55b3e8ef4
commit 2fe8e24fb1
No known key found for this signature in database
GPG key ID: B8DB720BC383E172
15 changed files with 278 additions and 7 deletions

View file

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

View file

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

View file

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

View file

@ -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!()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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())
}
}

View file

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

View file

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

View file

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