Add CLI handling, add command to generate/test configuration

This commit is contained in:
timvisee 2021-11-08 16:49:45 +01:00
parent fdc3c2ea3c
commit 66faac396a
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
14 changed files with 799 additions and 19 deletions

257
Cargo.lock generated
View File

@ -17,6 +17,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
[[package]]
name = "atty"
version = "0.2.14"
@ -64,6 +70,37 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"unicase",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
@ -73,6 +110,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
@ -82,6 +130,72 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "darling"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_builder"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@ -113,6 +227,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@ -181,6 +301,21 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -199,17 +334,43 @@ dependencies = [
"quick-error",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazymc"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"clap",
"colored",
"derive_builder",
"dotenv",
"futures",
"libc",
@ -217,6 +378,7 @@ dependencies = [
"minecraft-protocol",
"pretty_env_logger",
"serde",
"thiserror",
"tokio",
"toml",
]
@ -339,6 +501,15 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "os_str_bytes"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799"
dependencies = [
"memchr",
]
[[package]]
name = "pin-project-lite"
version = "0.2.7"
@ -361,6 +532,30 @@ dependencies = [
"log",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.32"
@ -563,6 +758,12 @@ dependencies = [
"libc",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.81"
@ -583,6 +784,35 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.13.0"
@ -622,6 +852,27 @@ dependencies = [
"serde",
]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
@ -638,6 +889,12 @@ dependencies = [
"serde",
]
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -19,15 +19,18 @@ exclude = [
edition = "2021"
[dependencies]
anyhow = "1.0"
bytes = "1.1"
clap = "3.0.0-beta.5"
colored = "2.0"
derive_builder = "0.10"
dotenv = "0.15"
futures = { version = "0.3", default-features = false }
libc = "0.2"
minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "4348c27" }
tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal"] }
dotenv = "0.15"
log = "0.4"
minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "4348c27" }
pretty_env_logger = "0.4"
serde = "1.0"
thiserror = "1.0"
tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal"] }
toml = "0.5"

View File

@ -5,19 +5,19 @@
#
# Public IP and port users will connect to.
# Shows sleeping status, starts server on connect, and proxies to ingress server.
address_egress = "0.0.0.0:25565"
address = "0.0.0.0:25565"
[server]
# Server directory.
directory = "."
# Command to start the server.
command = "java -Xms1G -Xmx1G -jar server.jar --nogui"
command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
# Ingress address.
#
# Internal IP and port of server started by lazymc to proxy to.
address_ingress = "127.0.0.1:25566"
address = "127.0.0.1:25566"
[time]
# Sleep after number of seconds.

View File

@ -0,0 +1,39 @@
use std::fs;
use std::path::PathBuf;
use clap::ArgMatches;
use crate::util::cli::prompt_yes;
use crate::util::error::{quit, quit_error, ErrorHintsBuilder};
/// Invoke config test command.
pub fn invoke(matches: &ArgMatches) {
// Get config path, attempt to canonicalize
let mut path = PathBuf::from(matches.value_of("config").unwrap());
if let Ok(p) = path.canonicalize() {
path = p;
}
// Confirm to overwrite if it exists
if path.is_file() {
if !prompt_yes(
&format!(
"Config file already exists, overwrite?\nPath: {}",
path.to_str().unwrap_or("?")
),
Some(true),
) {
quit();
}
}
// Generate file
if let Err(err) = fs::write(&path, include_bytes!("../../res/lazymc.toml")) {
quit_error(
anyhow!(err).context("Failed to generate config file"),
ErrorHintsBuilder::default().build().unwrap(),
);
}
eprintln!("Config saved at: {}", path.to_str().unwrap_or("?"));
}

38
src/action/config_test.rs Normal file
View File

@ -0,0 +1,38 @@
use std::path::PathBuf;
use clap::ArgMatches;
use crate::config::Config;
use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
/// Invoke config test command.
pub fn invoke(matches: &ArgMatches) {
// Get config path, attempt to canonicalize
let mut path = PathBuf::from(matches.value_of("config").unwrap());
if let Ok(p) = path.canonicalize() {
path = p;
}
// Ensure it exists
if !path.is_file() {
quit_error_msg(
format!("Config file does not exist at: {}", path.to_str().unwrap()),
ErrorHintsBuilder::default().build().unwrap(),
);
}
// Try to load config
let _config = match Config::load(path) {
Ok(config) => config,
Err(err) => {
quit_error(
anyhow!(err).context("Failed to load and parse config"),
ErrorHintsBuilder::default().build().unwrap(),
);
}
};
// TODO: do additional config tests: server dir correct, command set?
eprintln!("Config loaded successfully!");
}

3
src/action/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod config_generate;
pub mod config_test;
pub mod start;

1
src/action/start.rs Normal file
View File

@ -0,0 +1 @@
// Placeholder

33
src/cli.rs Normal file
View File

@ -0,0 +1,33 @@
use clap::{App, AppSettings, Arg};
/// The clap app for CLI argument parsing.
pub fn app() -> App<'static> {
App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.subcommand(
App::new("start")
.alias("run")
.about("Start lazymc and server (default)"),
)
.subcommand(
App::new("config")
.alias("cfg")
.about("Config actions")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(App::new("generate").alias("gen").about("Generate config"))
.subcommand(App::new("test").about("Test config")),
)
.arg(
Arg::new("config")
.short('c')
.alias("cfg")
.long("config")
.global(true)
.value_name("FILE")
.default_value(crate::config::CONFIG_FILE)
.about("Use config file")
.takes_value(true),
)
}

View File

@ -1,12 +1,12 @@
use std::fs;
use std::io;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use serde::Deserialize;
/// Default configuration file location.
const CONFIG_FILE: &str = "lazymc.toml";
pub const CONFIG_FILE: &str = "lazymc.toml";
/// Configuration.
#[derive(Debug, Deserialize)]
@ -26,8 +26,8 @@ pub struct Config {
impl Config {
/// Load configuration form file.
pub fn load() -> Result<Self, io::Error> {
let data = fs::read(CONFIG_FILE)?;
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
let data = fs::read(path)?;
let config = toml::from_slice(&data)?;
Ok(config)
}

View File

@ -1,17 +1,28 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate derive_builder;
#[macro_use]
extern crate log;
pub(crate) mod action;
pub(crate) mod cli;
pub(crate) mod config;
pub(crate) mod monitor;
pub(crate) mod proto;
pub(crate) mod server;
pub(crate) mod types;
pub(crate) mod util;
use std::error::Error;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use bytes::BytesMut;
use clap::{App, ArgMatches};
use futures::FutureExt;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::data::server_status::*;
@ -24,10 +35,12 @@ use tokio::io;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use crate::util::error::{quit_error, quit_error_msg, ErrorHints, ErrorHintsBuilder};
use config::*;
use proto::{Client, ClientState, RawPacket, PROTO_DEFAULT_PROTOCOL, PROTO_DEFAULT_VERSION};
use server::ServerState;
/// Main entrypoint.
#[tokio::main]
async fn main() -> Result<(), ()> {
// Initialize logging
@ -35,16 +48,79 @@ async fn main() -> Result<(), ()> {
let _ = dotenv::dotenv();
pretty_env_logger::init();
// Build clap app, invoke intended action
let app = cli::app();
invoke_action(app).await
}
/// Invoke an action.
async fn invoke_action<'a>(app: App<'a>) -> Result<(), ()> {
let matches = app.get_matches();
// Config operations
if let Some(ref matches) = matches.subcommand_matches("config") {
if let Some(ref matches) = matches.subcommand_matches("generate") {
action::config_generate::invoke(matches);
return Ok(());
}
if let Some(ref matches) = matches.subcommand_matches("test") {
action::config_test::invoke(matches);
return Ok(());
}
unimplemented!("Config logic here!");
}
// Start server
start(&matches).await
}
/// Load configuration file.
fn load_config(matches: &ArgMatches) -> Config {
// Get config path, attempt to canonicalize
let mut path = PathBuf::from(matches.value_of("config").unwrap());
if let Ok(p) = path.canonicalize() {
path = p;
}
// Ensure configuration file exists
if !path.is_file() {
quit_error_msg(
format!(
"Conig file does not exist: {}",
path.to_str().unwrap_or("?")
),
ErrorHintsBuilder::default()
.config(true)
.config_generate(true)
.build()
.unwrap(),
);
}
// Load config
let config = match Config::load() {
Ok(config) => Arc::new(config),
let config = match Config::load(path) {
Ok(config) => config,
Err(err) => {
error!("Failed to load configuration:");
error!("{}", err);
return Err(());
quit_error(
anyhow!(err).context("Failed to load config"),
ErrorHintsBuilder::default()
.config(true)
.config_test(true)
.build()
.unwrap(),
);
}
};
config
}
/// Start lazymc.
async fn start(matches: &ArgMatches) -> Result<(), ()> {
// Load config and server state
let config = Arc::new(load_config(matches));
let server_state = Arc::new(ServerState::default());
// Listen for new connections
@ -52,8 +128,10 @@ async fn main() -> Result<(), ()> {
let listener = TcpListener::bind(config.public.address)
.await
.map_err(|err| {
error!("Failed to start: {}", err);
()
quit_error(
anyhow!(err).context("Failed to start proxy server"),
ErrorHints::default(),
);
})?;
info!(

89
src/util/cli.rs Normal file
View File

@ -0,0 +1,89 @@
// Allow dead code, until we've fully implemented CLI/error handling
#![allow(dead_code)]
use std::io::{stderr, stdin, Write};
use crate::util::error::{quit_error, ErrorHints};
/// Prompt the user to enter some value.
/// The prompt that is shown should be passed to `msg`,
/// excluding the `:` suffix.
pub fn prompt(msg: &str) -> String {
// Show the prompt
eprint!("{}: ", msg);
let _ = stderr().flush();
// Get the input
let mut input = String::new();
if let Err(err) = stdin()
.read_line(&mut input)
.map_err(|err| -> anyhow::Error { err.into() })
{
quit_error(
err.context("failed to read input from prompt"),
ErrorHints::default(),
);
}
// Trim and return
input.trim().to_owned()
}
/// Prompt the user for a question, allowing a yes or now answer.
/// True is returned if yes was answered, false if no.
///
/// A default may be given, which is chosen if no-interact mode is
/// enabled, or if enter was pressed by the user without entering anything.
pub fn prompt_yes(msg: &str, def: Option<bool>) -> bool {
// Define the available options string
let options = format!(
"[{}/{}]",
match def {
Some(def) if def => "Y",
_ => "y",
},
match def {
Some(def) if !def => "N",
_ => "n",
}
);
// Get the user input
let answer = prompt(&format!("{} {}", msg, options));
// Assume the default if the answer is empty
if answer.is_empty() && def.is_some() {
return def.unwrap();
}
// Derive a boolean and return
match derive_bool(&answer) {
Some(answer) => answer,
None => prompt_yes(msg, def),
}
}
/// Try to derive true or false (yes or no) from the given input.
/// None is returned if no boolean could be derived accurately.
fn derive_bool(input: &str) -> Option<bool> {
// Process the input
let input = input.trim().to_lowercase();
// Handle short or incomplete answers
match input.as_str() {
"y" | "ye" | "t" | "1" => return Some(true),
"n" | "f" | "0" => return Some(false),
_ => {}
}
// Handle complete answers with any suffix
if input.starts_with("yes") || input.starts_with("true") {
return Some(true);
}
if input.starts_with("no") || input.starts_with("false") {
return Some(false);
}
// The answer could not be determined, return none
None
}

193
src/util/error.rs Normal file
View File

@ -0,0 +1,193 @@
// Allow dead code, until we've fully implemented CLI/error handling
#![allow(dead_code)]
use std::borrow::Borrow;
use std::fmt::{Debug, Display};
use std::io::{self, Write};
pub use std::process::exit;
use anyhow::anyhow;
use crate::util::style::{highlight, highlight_error, highlight_info, highlight_warning};
/// Print the given error in a proper format for the user,
/// with it's causes.
pub fn print_error(err: anyhow::Error) {
// Report each printable error, count them
let count = err
.chain()
.map(|err| format!("{}", err))
.filter(|err| !err.is_empty())
.enumerate()
.map(|(i, err)| {
if i == 0 {
eprintln!("{} {}", highlight_error("error:"), err);
} else {
eprintln!("{} {}", highlight_error("caused by:"), err);
}
})
.count();
// Fall back to a basic message
if count == 0 {
eprintln!(
"{} {}",
highlight_error("error:"),
"an undefined error occurred"
);
}
}
/// Print the given error message in a proper format for the user,
/// with it's causes.
pub fn print_error_msg<S>(err: S)
where
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
{
print_error(anyhow!(err));
}
/// Print a warning.
pub fn print_warning<S>(err: S)
where
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
{
eprintln!("{} {}", highlight_warning("warning:"), err);
}
/// Quit the application regularly.
pub fn quit() -> ! {
exit(0);
}
/// Quit the application with an error code,
/// and print the given error.
pub fn quit_error(err: anyhow::Error, hints: impl Borrow<ErrorHints>) -> ! {
// Print the error
print_error(err);
// Print error hints
hints.borrow().print(false);
// Quit
exit(1);
}
/// Quit the application with an error code,
/// and print the given error message.
pub fn quit_error_msg<S>(err: S, hints: impl Borrow<ErrorHints>) -> !
where
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
{
quit_error(anyhow!(err), hints);
}
/// The error hint configuration.
#[derive(Clone, Builder)]
#[builder(default)]
pub struct ErrorHints {
/// A list of info messages to print along with the error.
info: Vec<String>,
/// Show about the config flag.
config: bool,
/// Show about the config generate command.
config_generate: bool,
/// Show about the config test command.
config_test: 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.config || self.config_generate || self.config_test || self.verbose || self.help
}
/// Print the error hints.
pub fn print(&self, end_newline: bool) {
// Print info messages
for msg in &self.info {
eprintln!("{} {}", highlight_info("info:"), msg);
}
// Stop if nothing should be printed
if !self.any() {
return;
}
eprint!("\n");
// Print hints
let bin = crate::util::bin_name();
if self.config {
eprintln!(
"Use '{}' to select a config file",
highlight("--config FILE")
);
}
if self.config_generate {
eprintln!(
"Use '{}' to generate a new config file",
highlight(&format!("{} config generate", bin))
);
}
if self.config_test {
eprintln!(
"Use '{}' to test a config file",
highlight(&format!("{} config test -c FILE", bin))
);
}
if self.verbose {
eprintln!("For a detailed log add '{}'", highlight("--verbose"));
}
if self.help {
eprintln!("For more information add '{}'", highlight("--help"));
}
// End with additional newline
if end_newline {
eprintln!();
}
// Flush
let _ = io::stderr().flush();
}
}
impl Default for ErrorHints {
fn default() -> Self {
ErrorHints {
info: Vec::new(),
config: false,
config_generate: false,
config_test: false,
verbose: true,
help: true,
}
}
}
impl ErrorHintsBuilder {
/// Add a single info entry.
pub fn add_info(mut self, info: String) -> Self {
// Initialize the info list
if self.info.is_none() {
self.info = Some(Vec::new());
}
// Add the item to the info list
if let Some(ref mut list) = self.info {
list.push(info);
}
self
}
}

25
src/util/mod.rs Normal file
View File

@ -0,0 +1,25 @@
pub mod cli;
pub mod error;
pub mod style;
use std::env;
use std::path::PathBuf;
/// Get the name of the executable that was invoked.
///
/// When a symbolic or hard link is used, the name of the link is returned.
///
/// This attempts to obtain the binary name in the following order:
/// - name in first item of program arguments via `std::env::args`
/// - current executable name via `std::env::current_exe`
/// - crate name
pub fn bin_name() -> String {
env::args_os()
.next()
.filter(|path| !path.is_empty())
.map(PathBuf::from)
.or_else(|| env::current_exe().ok())
.and_then(|p| p.file_name().map(|n| n.to_owned()))
.and_then(|n| n.into_string().ok())
.unwrap_or_else(|| crate_name!().into())
}

21
src/util/style.rs Normal file
View File

@ -0,0 +1,21 @@
use colored::{ColoredString, Colorize};
/// Highlight the given text with a color.
pub fn highlight(msg: &str) -> ColoredString {
msg.yellow()
}
/// Highlight the given text with an error color.
pub fn highlight_error(msg: &str) -> ColoredString {
msg.red().bold()
}
/// Highlight the given text with an warning color.
pub fn highlight_warning(msg: &str) -> ColoredString {
highlight(msg).bold()
}
/// Highlight the given text with an info color
pub fn highlight_info(msg: &str) -> ColoredString {
msg.cyan()
}