use std::path::PathBuf; use clap::{IntoApp, Parser}; use log::LevelFilter; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; use crate::config::{self, ConfigFileFormat}; // Exit codes as defined in sysexits.h. pub const CONFIGURATION_ERROR_EXIT_CODE: i32 = 78; #[derive(Error, Debug)] pub enum CliError { #[error(transparent)] ArgParse(#[from] clap::Error), #[error(transparent)] Config(#[from] config::ConfigError), } pub trait AppOpts: Parser { fn try_init() -> Result<Self, CliError> { let result = Self::try_parse().map_err(CliError::ArgParse); match &result { Ok(result) => { try_init_pretty_logger( result.get_log_environment_variable_name(), result.get_log_level_filter(), ) .unwrap(); } Err(_) => { // Initialize the logger with defaults. pretty_env_logger::init(); } } result } fn init() -> Self { match Self::try_init() { Ok(args) => args, Err(CliError::ArgParse(err)) => err.exit(), Err(CliError::Config(err)) => { log::error!("{}", err); safe_exit(CONFIGURATION_ERROR_EXIT_CODE); } } } fn get_log_level_filter(&self) -> Option<LevelFilter> { None } fn get_log_environment_variable_name(&self) -> Option<&str> { None } } pub trait ConfigurableAppOpts<C: DeserializeOwned + Default + Serialize>: AppOpts { fn try_init_with_config() -> Result<(Self, C), CliError> { let opts = Self::try_init()?; let app = <Self as IntoApp>::command(); let conf = config::from_defaults( app.get_name(), &opts.get_additional_config_paths(), opts.get_config_file_format(), ) .map_err(CliError::Config)?; Ok((opts, conf)) } fn init_with_config() -> (Self, C) { match Self::try_init_with_config() { Ok(args_and_config) => args_and_config, Err(CliError::ArgParse(err)) => err.exit(), Err(CliError::Config(err)) => { log::error!("{}", err); safe_exit(CONFIGURATION_ERROR_EXIT_CODE); } } } fn get_config_file_format(&self) -> ConfigFileFormat { ConfigFileFormat::Toml } fn get_additional_config_paths(&self) -> Vec<(PathBuf, Option<ConfigFileFormat>)>; } fn try_init_pretty_logger( environment_variable_name: Option<&str>, level_filter_override: Option<LevelFilter>, ) -> Result<(), log::SetLoggerError> { let mut builder = pretty_env_logger::formatted_builder(); if let Some(level_filter_override) = level_filter_override { builder.filter(None, level_filter_override); } if let Ok(s) = ::std::env::var(environment_variable_name.unwrap_or("RUST_LOG")) { builder.parse_filters(&s); } builder.try_init() } /// Converts a integer verbosity value into a `LevelFilter`. /// /// Examples: /// /// ``` /// use collective::cli::get_log_level_filter_from_verbosity; /// /// assert_eq!( /// get_log_level_filter_from_verbosity(1), /// Some(log::LevelFilter::Info) /// ); /// /// assert_eq!(get_log_level_filter_from_verbosity(15), None); /// ``` pub fn get_log_level_filter_from_verbosity(verbosity: u8) -> Option<log::LevelFilter> { match verbosity { 3 => Some(log::LevelFilter::Trace), 2 => Some(log::LevelFilter::Debug), 1 => Some(log::LevelFilter::Info), _ => None, } } /// Flushes stdout/stderr and terminates the current process. /// /// This is used internally in the library to handle non-critical CLI errors. pub fn safe_exit(code: i32) -> ! { use std::io::Write; let _ = std::io::stdout().lock().flush(); let _ = std::io::stderr().lock().flush(); std::process::exit(code) }