소스 검색

Implement configurable error hints

timvisee 7 년 전
부모
커밋
c377e2be51
7개의 변경된 파일177개의 추가작업 그리고 46개의 파일을 삭제
  1. 1 0
      Cargo.lock
  2. 1 0
      cli/Cargo.toml
  3. 21 9
      cli/src/action/download.rs
  4. 23 8
      cli/src/cmd/arg/host.rs
  5. 23 8
      cli/src/cmd/arg/url.rs
  6. 3 2
      cli/src/main.rs
  7. 105 19
      cli/src/util.rs

+ 1 - 0
Cargo.lock

@@ -361,6 +361,7 @@ dependencies = [
  "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "ffsend-api 0.1.0",
  "ffsend-api 0.1.0",

+ 1 - 0
cli/Cargo.toml

@@ -18,6 +18,7 @@ no-color = ["colored/no-color"]
 clap = "2.31"
 clap = "2.31"
 clipboard = { version = "0.4", optional = true }
 clipboard = { version = "0.4", optional = true }
 colored = "1.6"
 colored = "1.6"
+derive_builder = "0.5"
 failure = "0.1"
 failure = "0.1"
 failure_derive = "0.1"
 failure_derive = "0.1"
 ffsend-api = { version = "*", path = "../api" }
 ffsend-api = { version = "*", path = "../api" }

+ 21 - 9
cli/src/action/download.rs

@@ -4,7 +4,7 @@ use std::path::{self, PathBuf};
 use std::sync::{Arc, Mutex};
 use std::sync::{Arc, Mutex};
 
 
 use clap::ArgMatches;
 use clap::ArgMatches;
-use failure::{err_msg, Fail};
+use failure::Fail;
 use ffsend_api::action::download::{
 use ffsend_api::action::download::{
     Download as ApiDownload,
     Download as ApiDownload,
     Error as DownloadError,
     Error as DownloadError,
@@ -26,7 +26,14 @@ use cmd::matcher::{
     main::MainMatcher,
     main::MainMatcher,
 };
 };
 use progress::ProgressBar;
 use progress::ProgressBar;
-use util::{ensure_password, prompt_yes, quit, quit_error};
+use util::{
+    ensure_password,
+    ErrorHints,
+    prompt_yes,
+    quit,
+    quit_error,
+    quit_error_msg,
+};
 
 
 /// A file download action.
 /// A file download action.
 pub struct Download<'a> {
 pub struct Download<'a> {
@@ -153,10 +160,13 @@ impl<'a> Download<'a> {
                 if let Err(err) = create_dir_all(parent) {
                 if let Err(err) = create_dir_all(parent) {
                     quit_error(err.context(
                     quit_error(err.context(
                         "Failed to create parent directories for output file",
                         "Failed to create parent directories for output file",
-                    ));
+                    ), ErrorHints::default());
                 }
                 }
             },
             },
-            None => quit_error(err_msg("Invalid output file path").compat()),
+            None => quit_error_msg(
+                "Invalid output file path",
+                ErrorHints::default(),
+            ),
         }
         }
 
 
         return target;
         return target;
@@ -175,7 +185,8 @@ impl<'a> Download<'a> {
             match target.canonicalize() {
             match target.canonicalize() {
                 Ok(target) => return target,
                 Ok(target) => return target,
                 Err(err) => quit_error(
                 Err(err) => quit_error(
-                    err.context("Failed to canonicalize target path")
+                    err.context("Failed to canonicalize target path"),
+                    ErrorHints::default(),
                 ),
                 ),
             }
             }
         }
         }
@@ -185,7 +196,8 @@ impl<'a> Download<'a> {
             match target.canonicalize() {
             match target.canonicalize() {
                 Ok(target) => return target.join(name_hint),
                 Ok(target) => return target.join(name_hint),
                 Err(err) => quit_error(
                 Err(err) => quit_error(
-                    err.context("Failed to canonicalize target path")
+                    err.context("Failed to canonicalize target path"),
+                    ErrorHints::default(),
                 ),
                 ),
             }
             }
         }
         }
@@ -204,7 +216,7 @@ impl<'a> Download<'a> {
                 Ok(target) => return target.join(name_hint),
                 Ok(target) => return target.join(name_hint),
                 Err(err) => quit_error(err.context(
                 Err(err) => quit_error(err.context(
                     "Failed to determine working directory to use for the output file"
                     "Failed to determine working directory to use for the output file"
-                )),
+                ), ErrorHints::default()),
             }
             }
         }
         }
         let path = path.unwrap();
         let path = path.unwrap();
@@ -222,8 +234,8 @@ impl<'a> Download<'a> {
             match current_dir() {
             match current_dir() {
                 Ok(workdir) => target = workdir.join(target),
                 Ok(workdir) => target = workdir.join(target),
                 Err(err) => quit_error(err.context(
                 Err(err) => quit_error(err.context(
-                    "Failed to determine working directory to use for the output file"
-                )),
+                        "Failed to determine working directory to use for the output file"
+                    ), ErrorHints::default()),
             }
             }
         }
         }
 
 

+ 23 - 8
cli/src/cmd/arg/host.rs

@@ -3,7 +3,7 @@ use ffsend_api::url::{ParseError, Url};
 
 
 use app::SEND_DEF_HOST;
 use app::SEND_DEF_HOST;
 use super::{CmdArg, CmdArgOption};
 use super::{CmdArg, CmdArgOption};
-use util::quit_error_msg;
+use util::{ErrorHints, quit_error_msg};
 
 
 /// The host argument.
 /// The host argument.
 pub struct ArgHost { }
 pub struct ArgHost { }
@@ -36,18 +36,33 @@ impl<'a> CmdArgOption<'a> for ArgHost {
         match Url::parse(url) {
         match Url::parse(url) {
             Ok(url) => url,
             Ok(url) => url,
             Err(ParseError::EmptyHost) =>
             Err(ParseError::EmptyHost) =>
-                quit_error_msg("Emtpy host given"),
+                quit_error_msg("Emtpy host given", ErrorHints::default()),
             Err(ParseError::InvalidPort) =>
             Err(ParseError::InvalidPort) =>
-                quit_error_msg("Invalid host port"),
+                quit_error_msg("Invalid host port", ErrorHints::default()),
             Err(ParseError::InvalidIpv4Address) =>
             Err(ParseError::InvalidIpv4Address) =>
-                quit_error_msg("Invalid IPv4 address in host"),
+                quit_error_msg(
+                    "Invalid IPv4 address in host",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::InvalidIpv6Address) =>
             Err(ParseError::InvalidIpv6Address) =>
-                quit_error_msg("Invalid IPv6 address in host"),
+                quit_error_msg(
+                    "Invalid IPv6 address in host",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::InvalidDomainCharacter) =>
             Err(ParseError::InvalidDomainCharacter) =>
-                quit_error_msg("Host domains contains an invalid character"),
+                quit_error_msg(
+                    "Host domains contains an invalid character",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::RelativeUrlWithoutBase) =>
             Err(ParseError::RelativeUrlWithoutBase) =>
-                quit_error_msg("Host domain doesn't contain a host"),
-            _ => quit_error_msg("The given host is invalid"),
+                quit_error_msg(
+                    "Host domain doesn't contain a host",
+                    ErrorHints::default(),
+                ),
+            _ => quit_error_msg(
+                    "The given host is invalid",
+                    ErrorHints::default(),
+                ),
         }
         }
     }
     }
 }
 }

+ 23 - 8
cli/src/cmd/arg/url.rs

@@ -2,7 +2,7 @@ use clap::{Arg, ArgMatches};
 use ffsend_api::url::{ParseError, Url};
 use ffsend_api::url::{ParseError, Url};
 
 
 use super::{CmdArg, CmdArgOption};
 use super::{CmdArg, CmdArgOption};
-use util::quit_error_msg;
+use util::{ErrorHints, quit_error_msg};
 
 
 /// The URL argument.
 /// The URL argument.
 pub struct ArgUrl { }
 pub struct ArgUrl { }
@@ -32,18 +32,33 @@ impl<'a> CmdArgOption<'a> for ArgUrl {
         match Url::parse(url) {
         match Url::parse(url) {
             Ok(url) => url,
             Ok(url) => url,
             Err(ParseError::EmptyHost) =>
             Err(ParseError::EmptyHost) =>
-                quit_error_msg("Emtpy host given"),
+                quit_error_msg("Emtpy host given", ErrorHints::default()),
             Err(ParseError::InvalidPort) =>
             Err(ParseError::InvalidPort) =>
-                quit_error_msg("Invalid host port"),
+                quit_error_msg("Invalid host port", ErrorHints::default()),
             Err(ParseError::InvalidIpv4Address) =>
             Err(ParseError::InvalidIpv4Address) =>
-                quit_error_msg("Invalid IPv4 address in host"),
+                quit_error_msg(
+                    "Invalid IPv4 address in host",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::InvalidIpv6Address) =>
             Err(ParseError::InvalidIpv6Address) =>
-                quit_error_msg("Invalid IPv6 address in host"),
+                quit_error_msg(
+                    "Invalid IPv6 address in host",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::InvalidDomainCharacter) =>
             Err(ParseError::InvalidDomainCharacter) =>
-                quit_error_msg("Host domains contains an invalid character"),
+                quit_error_msg(
+                    "Host domains contains an invalid character",
+                    ErrorHints::default(),
+                ),
             Err(ParseError::RelativeUrlWithoutBase) =>
             Err(ParseError::RelativeUrlWithoutBase) =>
-                quit_error_msg("Host domain doesn't contain a host"),
-            _ => quit_error_msg("The given host is invalid"),
+                quit_error_msg(
+                    "Host domain doesn't contain a host",
+                    ErrorHints::default(),
+                ),
+            _ => quit_error_msg(
+                    "The given host is invalid",
+                    ErrorHints::default(),
+                ),
         }
         }
     }
     }
 }
 }

+ 3 - 2
cli/src/main.rs

@@ -1,5 +1,6 @@
 extern crate clap;
 extern crate clap;
 #[macro_use]
 #[macro_use]
+extern crate derive_builder;
 extern crate failure;
 extern crate failure;
 #[macro_use]
 #[macro_use]
 extern crate failure_derive;
 extern crate failure_derive;
@@ -22,7 +23,7 @@ use action::password::Password;
 use action::upload::Upload;
 use action::upload::Upload;
 use cmd::Handler;
 use cmd::Handler;
 use error::Error;
 use error::Error;
-use util::quit_error;
+use util::{ErrorHints, quit_error};
 
 
 /// Application entrypoint.
 /// Application entrypoint.
 fn main() {
 fn main() {
@@ -31,7 +32,7 @@ fn main() {
 
 
     // Invoke the proper action
     // Invoke the proper action
     if let Err(err) = invoke_action(&cmd_handler) {
     if let Err(err) = invoke_action(&cmd_handler) {
-        quit_error(err);
+        quit_error(err, ErrorHints::default());
     };
     };
 }
 }
 
 

+ 105 - 19
cli/src/util.rs

@@ -17,7 +17,7 @@ use std::process::{exit, ExitStatus};
 #[cfg(feature = "clipboard")]
 #[cfg(feature = "clipboard")]
 use self::clipboard::{ClipboardContext, ClipboardProvider};
 use self::clipboard::{ClipboardContext, ClipboardProvider};
 use self::colored::*;
 use self::colored::*;
-use failure::{self, err_msg, Fail};
+use failure::{err_msg, Fail};
 use ffsend_api::url::Url;
 use ffsend_api::url::Url;
 use rpassword::prompt_password_stderr;
 use rpassword::prompt_password_stderr;
 
 
@@ -55,13 +55,12 @@ pub fn quit() -> ! {
 
 
 /// Quit the application with an error code,
 /// Quit the application with an error code,
 /// and print the given error.
 /// and print the given error.
-pub fn quit_error<E: Fail>(err: E) -> ! {
+pub fn quit_error<E: Fail>(err: E, hints: ErrorHints) -> ! {
     // Print the error
     // Print the error
     print_error(err);
     print_error(err);
 
 
-    // Print some additional information
-    eprintln!("\nFor detailed errors try '{}'", "--verbose".yellow());
-    eprintln!("For more information try '{}'", "--help".yellow());
+    // Print error hints
+    hints.print();
 
 
     // Quit
     // Quit
     exit(1);
     exit(1);
@@ -69,11 +68,80 @@ pub fn quit_error<E: Fail>(err: E) -> ! {
 
 
 /// Quit the application with an error code,
 /// Quit the application with an error code,
 /// and print the given error message.
 /// and print the given error message.
-pub fn quit_error_msg<S>(err: S) -> !
+pub fn quit_error_msg<S>(err: S, hints: ErrorHints) -> !
     where
     where
         S: AsRef<str> + Display + Debug + Sync + Send + 'static
         S: AsRef<str> + Display + Debug + Sync + Send + 'static
 {
 {
-    quit_error(failure::err_msg(err).compat());
+    quit_error(err_msg(err).compat(), hints);
+}
+
+/// The error hint configuration.
+#[derive(Copy, Clone, Builder)]
+#[builder(default)]
+pub struct ErrorHints {
+    /// Show about the password option.
+    password: bool,
+
+    /// Show about the owner option.
+    owner: bool,
+
+    /// Show about the force flag.
+    force: bool,
+
+    /// Show about the verbose flag.
+    verbose: bool,
+
+    /// Show about the help flag.
+    help: bool,
+}
+
+impl ErrorHints {
+    /// Check whether any hint should be printed.
+    pub fn any(&self) -> bool {
+        self.password || self.owner || self.force || self.verbose || self.help
+    }
+
+    /// Print the error hints.
+    pub fn print(&self) {
+        // Stop if nothing should be printed
+        if !self.any() {
+            return;
+        }
+
+        eprint!("\n");
+
+        // Print hints
+        if self.password {
+            eprintln!("Use '{}' to specify a password", "--password <PASSWORD>".yellow());
+        }
+        if self.owner {
+            eprintln!("Use '{}' to specify an owner token", "--owner <TOKEN>".yellow());
+        }
+        if self.force {
+            eprintln!("Use '{}' to force", "--force".yellow());
+        }
+        if self.verbose {
+            eprintln!("For detailed errors try '{}'", "--verbose".yellow());
+        }
+        if self.help {
+            eprintln!("For more information try '{}'", "--help".yellow());
+        }
+
+        // Flush
+        let _ = stderr().flush();
+    }
+}
+
+impl Default for ErrorHints {
+    fn default() -> Self {
+        ErrorHints {
+            password: false,
+            owner: false,
+            force: false,
+            verbose: true,
+            help: true,
+        }
+    }
 }
 }
 
 
 /// Open the given URL in the users default browser.
 /// Open the given URL in the users default browser.
@@ -100,7 +168,14 @@ pub fn set_clipboard(content: String) -> Result<(), Box<StdError>> {
 pub fn prompt_password(main_matcher: &MainMatcher) -> String {
 pub fn prompt_password(main_matcher: &MainMatcher) -> String {
     // Quit with an error if we may not interact
     // Quit with an error if we may not interact
     if main_matcher.no_interact() {
     if main_matcher.no_interact() {
-        quit_error(err_msg("Missing password, must be specified in no-interact mode").compat());
+        quit_error_msg(
+            "Missing password, must be specified in no-interact mode",
+            ErrorHintsBuilder::default()
+                .password(true)
+                .verbose(false)
+                .build()
+                .unwrap(),
+        );
     }
     }
 
 
     // Prompt for the password
     // Prompt for the password
@@ -108,15 +183,19 @@ pub fn prompt_password(main_matcher: &MainMatcher) -> String {
         Ok(password) => password,
         Ok(password) => password,
         Err(err) => quit_error(err.context(
         Err(err) => quit_error(err.context(
             "Failed to read password from password prompt"
             "Failed to read password from password prompt"
-        )),
+        ), ErrorHints::default()),
     };
     };
 
 
     // Do not allow empty passwords unless forced
     // Do not allow empty passwords unless forced
     if !main_matcher.force() && password.is_empty() {
     if !main_matcher.force() && password.is_empty() {
-        quit_error(err_msg("\
-            An empty password is not supported by the web interface, \
-            use '-f' to force\
-        ").compat())
+        quit_error_msg(
+            "An empty password is not supported by the web interface",
+            ErrorHintsBuilder::default()
+                .force(true)
+                .verbose(false)
+                .build()
+                .unwrap(),
+        )
     }
     }
 
 
     password
     password
@@ -155,10 +234,10 @@ pub fn ensure_password(
 pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
 pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
     // Quit with an error if we may not interact
     // Quit with an error if we may not interact
     if main_matcher.no_interact() {
     if main_matcher.no_interact() {
-        quit_error(format_err!(
+        quit_error_msg(format!(
             "Could not prompt for '{}' in no-interact mode, maybe specify it",
             "Could not prompt for '{}' in no-interact mode, maybe specify it",
             msg,
             msg,
-        ).compat());
+        ), ErrorHints::default());
     }
     }
 
 
     // Show the prompt
     // Show the prompt
@@ -170,7 +249,7 @@ pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
     if let Err(err) = stdin().read_line(&mut input) {
     if let Err(err) = stdin().read_line(&mut input) {
         quit_error(err.context(
         quit_error(err.context(
             "Failed to read input from prompt"
             "Failed to read input from prompt"
-        ));
+        ), ErrorHints::default());
     }
     }
 
 
     // Trim and return
     // Trim and return
@@ -212,10 +291,10 @@ pub fn prompt_yes(
             });
             });
             return def;
             return def;
         } else {
         } else {
-            quit_error(format_err!(
+            quit_error_msg(format!(
                 "Could not prompt question '{}' in no-interact mode, maybe specify it",
                 "Could not prompt question '{}' in no-interact mode, maybe specify it",
                 msg,
                 msg,
-            ).compat());
+            ), ErrorHints::default());
         }
         }
     }
     }
 
 
@@ -287,7 +366,14 @@ pub fn ensure_owner_token(
             if interact {
             if interact {
                 *token = Some(prompt_owner_token(main_matcher));
                 *token = Some(prompt_owner_token(main_matcher));
             } else {
             } else {
-                quit_error(err_msg("Missing owner token, must be specified in no-interact mode").compat());
+                quit_error_msg(
+                    "Missing owner token, must be specified in no-interact mode",
+                    ErrorHintsBuilder::default()
+                        .owner(true)
+                        .verbose(false)
+                        .build()
+                        .unwrap(),
+                );
             }
             }
         }
         }