mirror of
https://github.com/Hopiu/lychee.git
synced 2026-03-17 05:00:26 +00:00
Customize verbosity (#956)
This commit is contained in:
parent
1350a0bdf6
commit
7874195bbb
7 changed files with 159 additions and 163 deletions
2
fixtures/configs/smoketest.toml
vendored
2
fixtures/configs/smoketest.toml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
############################# Display #############################
|
||||
|
||||
# Verbose program output
|
||||
verbose = true
|
||||
verbose = "warn"
|
||||
|
||||
# Don't show interactive progress bar while checking links.
|
||||
no_progress = true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use tokio_stream::wrappers::ReceiverStream;
|
|||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::formatters::response::ResponseFormatter;
|
||||
use crate::verbosity::{Verbosity, WarnLevel};
|
||||
use crate::verbosity::Verbosity;
|
||||
use crate::{cache::Cache, stats::ResponseStats, ExitCode};
|
||||
use lychee_lib::{Client, Request, Response};
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ where
|
|||
let (send_req, recv_req) = mpsc::channel(params.cfg.max_concurrency);
|
||||
let (send_resp, recv_resp) = mpsc::channel(params.cfg.max_concurrency);
|
||||
let max_concurrency = params.cfg.max_concurrency;
|
||||
let stats = if params.cfg.verbose.is_verbose() {
|
||||
let stats = if params.cfg.verbose.log_level() >= log::Level::Info {
|
||||
ResponseStats::extended()
|
||||
} else {
|
||||
ResponseStats::default()
|
||||
|
|
@ -113,7 +113,7 @@ where
|
|||
/// Reads from the request channel and updates the progress bar status
|
||||
async fn progress_bar_task(
|
||||
mut recv_resp: mpsc::Receiver<Response>,
|
||||
verbose: Verbosity<WarnLevel>,
|
||||
verbose: Verbosity,
|
||||
pb: Option<ProgressBar>,
|
||||
formatter: Arc<Box<dyn ResponseFormatter>>,
|
||||
mut stats: ResponseStats,
|
||||
|
|
@ -212,16 +212,16 @@ fn show_progress(
|
|||
progress_bar: &Option<ProgressBar>,
|
||||
response: &Response,
|
||||
formatter: &Arc<Box<dyn ResponseFormatter>>,
|
||||
verbose: &Verbosity<WarnLevel>,
|
||||
verbose: &Verbosity,
|
||||
) -> Result<()> {
|
||||
let out = formatter.write_response(response)?;
|
||||
if let Some(pb) = progress_bar {
|
||||
pb.inc(1);
|
||||
pb.set_message(out.clone());
|
||||
if verbose.is_verbose() {
|
||||
if verbose.log_level() >= log::Level::Info {
|
||||
pb.println(out);
|
||||
}
|
||||
} else if verbose.is_verbose()
|
||||
} else if verbose.log_level() >= log::Level::Info
|
||||
|| (!response.status().is_success() && !response.status().is_excluded())
|
||||
{
|
||||
writeln!(output, "{out}")?;
|
||||
|
|
@ -255,11 +255,30 @@ mod tests {
|
|||
&None,
|
||||
&response,
|
||||
&formatter,
|
||||
&Verbosity::new(0, 0),
|
||||
&Verbosity::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
info!("{:?}", String::from_utf8_lossy(&buf));
|
||||
assert!(buf.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_show_cached_responses_in_progress_debug_output() {
|
||||
let mut buf = Vec::new();
|
||||
let response = Response(
|
||||
InputSource::Stdin,
|
||||
ResponseBody {
|
||||
uri: Uri::try_from("http://127.0.0.1").unwrap(),
|
||||
status: Status::Cached(CacheStatus::Ok(200)),
|
||||
},
|
||||
);
|
||||
let formatter: Arc<Box<dyn ResponseFormatter>> =
|
||||
Arc::new(Box::new(formatters::response::Raw::new()));
|
||||
show_progress(&mut buf, &None, &response, &formatter, &Verbosity::debug()).unwrap();
|
||||
|
||||
assert!(!buf.is_empty());
|
||||
let buf = String::from_utf8_lossy(&buf);
|
||||
assert_eq!(buf, "↻ [200] http://127.0.0.1/ | Cached: OK (cached)\n");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use std::path::PathBuf;
|
|||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::verbosity::Verbosity;
|
||||
use crate::verbosity::WarnLevel;
|
||||
use crate::ExitCode;
|
||||
|
||||
use super::CommandParams;
|
||||
|
|
@ -57,7 +56,7 @@ where
|
|||
|
||||
let excluded = params.client.is_excluded(&request.uri);
|
||||
|
||||
if excluded && !params.cfg.verbose.is_verbose() {
|
||||
if excluded && params.cfg.verbose.log_level() < log::Level::Info {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = write(&mut writer, &request, ¶ms.cfg.verbose, excluded) {
|
||||
|
|
@ -75,17 +74,17 @@ where
|
|||
fn write(
|
||||
writer: &mut Box<dyn Write>,
|
||||
request: &Request,
|
||||
verbosity: &Verbosity<WarnLevel>,
|
||||
verbosity: &Verbosity,
|
||||
excluded: bool,
|
||||
) -> io::Result<()> {
|
||||
// Only print `data:` URIs if verbose mode is enabled
|
||||
if request.uri.is_data() && !verbosity.is_verbose() {
|
||||
// Only print `data:` URIs if verbose mode is at least `info`.
|
||||
if request.uri.is_data() && verbosity.log_level() < log::Level::Info {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let request = if verbosity.is_verbose() {
|
||||
// Only print source in verbose mode. This way the normal link output
|
||||
// can be fed into another tool without data mangling.
|
||||
// Only print source if verbose mode is at least `info`. This way the normal
|
||||
// link output can be fed into another tool without data mangling.
|
||||
let request = if verbosity.log_level() >= log::Level::Info {
|
||||
request.to_string()
|
||||
} else {
|
||||
request.uri.to_string()
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ async fn run(opts: &LycheeOptions) -> Result<i32> {
|
|||
if let Some(output) = &opts.config.output {
|
||||
fs::write(output, formatted).context("Cannot write status output to file")?;
|
||||
} else {
|
||||
if opts.config.verbose.log_level() == Some(log::Level::Debug) && !is_empty {
|
||||
if opts.config.verbose.log_level() >= log::Level::Info && !is_empty {
|
||||
// separate summary from the verbose list of links above
|
||||
// with a newline
|
||||
writeln!(io::stdout())?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::parse::{parse_base, parse_statuscodes};
|
||||
use crate::verbosity::{Verbosity, WarnLevel};
|
||||
use crate::verbosity::Verbosity;
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use clap::{arg, Parser};
|
||||
use const_format::{concatcp, formatcp};
|
||||
|
|
@ -83,7 +83,7 @@ default_function! {
|
|||
timeout: usize = DEFAULT_TIMEOUT_SECS;
|
||||
retry_wait_time: usize = DEFAULT_RETRY_WAIT_TIME_SECS;
|
||||
method: String = DEFAULT_METHOD.to_string();
|
||||
verbosity: Verbosity<WarnLevel> = Verbosity::new(1, 0);
|
||||
verbosity: Verbosity = Verbosity::default();
|
||||
}
|
||||
|
||||
// Macro for merging configuration values
|
||||
|
|
@ -144,7 +144,7 @@ pub(crate) struct Config {
|
|||
/// Verbose program output
|
||||
#[clap(flatten)]
|
||||
#[serde(default = "verbosity")]
|
||||
pub(crate) verbose: Verbosity<WarnLevel>,
|
||||
pub(crate) verbose: Verbosity,
|
||||
|
||||
/// Do not show progress bar.
|
||||
/// This is recommended for non-interactive shells (e.g. for continuous integration)
|
||||
|
|
@ -365,7 +365,7 @@ impl Config {
|
|||
self, toml;
|
||||
|
||||
// Keys with defaults to assign
|
||||
verbose: Verbosity::new(1, 0);
|
||||
verbose: Verbosity::default();
|
||||
cache: false;
|
||||
no_progress: false;
|
||||
max_redirects: DEFAULT_MAX_REDIRECTS;
|
||||
|
|
|
|||
|
|
@ -1,202 +1,165 @@
|
|||
//! Control `log` level with a `--verbose` flag for your CLI
|
||||
//!
|
||||
//! The original source is from
|
||||
//! [this crate](https://github.com/clap-rs/clap-verbosity-flag).
|
||||
//! Modifications were made to add support for serializing the `Verbosity`
|
||||
//! struct and to add a convenience method to get the verbosity status.
|
||||
//! Easily add a `--verbose` flag to CLIs using Structopt
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten`
|
||||
//! [`Verbosity`]:
|
||||
//! ```rust,no_run
|
||||
//! # use clap::Parser;
|
||||
//! # use clap_verbosity_flag::Verbosity;
|
||||
//! #
|
||||
//! # /// Le CLI
|
||||
//! # #[derive(Debug, Parser)]
|
||||
//! # struct Cli {
|
||||
//! #[command(flatten)]
|
||||
//! verbose: Verbosity,
|
||||
//! # }
|
||||
//! ```
|
||||
//! use clap::Parser;
|
||||
//! use clap_verbosity_flag::Verbosity;
|
||||
//!
|
||||
//! /// Le CLI
|
||||
//! #[derive(Debug, Parser)]
|
||||
//! struct Cli {
|
||||
//! #[command(flatten)]
|
||||
//! verbose: Verbosity,
|
||||
//! }
|
||||
//!
|
||||
//! You can then use this to configure your logger:
|
||||
//! ```rust,no_run
|
||||
//! # use clap::Parser;
|
||||
//! # use clap_verbosity_flag::Verbosity;
|
||||
//! #
|
||||
//! # /// Le CLI
|
||||
//! # #[derive(Debug, Parser)]
|
||||
//! # struct Cli {
|
||||
//! # #[command(flatten)]
|
||||
//! # verbose: Verbosity,
|
||||
//! # }
|
||||
//! let cli = Cli::parse();
|
||||
//! env_logger::Builder::new()
|
||||
//! .filter_level(cli.verbose.log_level_filter())
|
||||
//! .init();
|
||||
//! ```
|
||||
//!
|
||||
//! By default, this will only report errors.
|
||||
//! This will only report errors.
|
||||
//! - `-q` silences output
|
||||
//! - `-v` show warnings
|
||||
//! - `-vv` show info
|
||||
//! - `-vvv` show debug
|
||||
//! - `-vvvv` show trace
|
||||
//!
|
||||
//! You can also customize the default logging level:
|
||||
//! ```rust,no_run
|
||||
//! # use clap::Parser;
|
||||
//! use clap_verbosity_flag::{Verbosity, InfoLevel};
|
||||
//!
|
||||
//! /// Le CLI
|
||||
//! #[derive(Debug, Parser)]
|
||||
//! struct Cli {
|
||||
//! #[command(flatten)]
|
||||
//! verbose: Verbosity<InfoLevel>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Or implement [`LogLevel`] yourself for more control.
|
||||
|
||||
#[derive(clap::Args, Eq, PartialEq, Debug, Deserialize, Clone)]
|
||||
pub(crate) struct Verbosity<L: LogLevel = ErrorLevel> {
|
||||
#[clap(
|
||||
use log::Level;
|
||||
use log::LevelFilter;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(clap::Args, Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Verbosity {
|
||||
/// Pass many times for more log output
|
||||
///
|
||||
/// By default, it'll only report errors and warnings.
|
||||
/// Passing `-v` one time also prints info,
|
||||
/// `-vv` enables debugging logging, `-vvv` trace.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'v',
|
||||
action = clap::ArgAction::Count,
|
||||
global = true,
|
||||
help = L::verbose_help(),
|
||||
long_help = L::verbose_long_help(),
|
||||
help = Self::verbose_help(),
|
||||
long_help = Self::verbose_long_help(),
|
||||
conflicts_with = "quiet",
|
||||
)]
|
||||
verbose: u8,
|
||||
|
||||
#[clap(
|
||||
#[arg(
|
||||
long,
|
||||
short = 'q',
|
||||
action = clap::ArgAction::Count,
|
||||
global = true,
|
||||
help = L::quiet_help(),
|
||||
long_help = L::quiet_long_help(),
|
||||
help = Self::quiet_help(),
|
||||
long_help = Self::quiet_long_help(),
|
||||
conflicts_with = "verbose",
|
||||
)]
|
||||
quiet: u8,
|
||||
|
||||
#[clap(skip)]
|
||||
phantom: std::marker::PhantomData<L>,
|
||||
}
|
||||
|
||||
impl<L: LogLevel> Verbosity<L> {
|
||||
/// Create a new verbosity instance by explicitly setting the values
|
||||
pub(crate) const fn new(verbose: u8, quiet: u8) -> Self {
|
||||
Verbosity {
|
||||
verbose,
|
||||
quiet,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl Verbosity {
|
||||
/// Get the log level.
|
||||
///
|
||||
/// `None` means all output is disabled.
|
||||
pub(crate) fn log_level(&self) -> Option<log::Level> {
|
||||
pub(crate) const fn log_level(&self) -> Level {
|
||||
level_enum(self.verbosity())
|
||||
}
|
||||
|
||||
/// Get the log level filter.
|
||||
pub(crate) fn log_level_filter(&self) -> log::LevelFilter {
|
||||
level_enum(self.verbosity()).map_or(log::LevelFilter::Off, |l| l.to_level_filter())
|
||||
pub(crate) fn log_level_filter(&self) -> LevelFilter {
|
||||
level_enum(self.verbosity()).to_level_filter()
|
||||
}
|
||||
|
||||
/// Shorthand to check if the user requested "more verbose" output
|
||||
/// (assuming the default log level is `Warn`)
|
||||
pub(crate) fn is_verbose(&self) -> bool {
|
||||
self.log_level() >= log::LevelFilter::Info.to_level()
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
const fn verbosity(&self) -> i8 {
|
||||
level_value(log::Level::Warn) - (self.quiet as i8) + (self.verbose as i8)
|
||||
}
|
||||
|
||||
fn verbosity(&self) -> i8 {
|
||||
#![allow(clippy::cast_possible_wrap)]
|
||||
level_value(L::default()) - (self.quiet as i8) + (self.verbose as i8)
|
||||
const fn verbose_help() -> &'static str {
|
||||
"Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)"
|
||||
}
|
||||
|
||||
const fn verbose_long_help() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
const fn quiet_help() -> &'static str {
|
||||
"Less output per occurrence (e.g. `-q` or `-qq`)"
|
||||
}
|
||||
|
||||
const fn quiet_long_help() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const fn level_value(level: Option<log::Level>) -> i8 {
|
||||
#[cfg(test)]
|
||||
impl Verbosity {
|
||||
pub(crate) const fn debug() -> Self {
|
||||
Self {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
verbose: level_value(log::Level::Debug) as u8,
|
||||
quiet: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Deserialize for `Verbosity`
|
||||
// This can be deserialized from a string like "warn", "warning", or "Warning"
|
||||
// for example
|
||||
impl<'de> Deserialize<'de> for Verbosity {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let level = match s.to_lowercase().as_str() {
|
||||
"error" => Level::Error,
|
||||
"warn" | "warning" => Level::Warn,
|
||||
"info" => Level::Info,
|
||||
"debug" => Level::Debug,
|
||||
"trace" => Level::Trace,
|
||||
level => {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid log level `{level}`"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(Verbosity {
|
||||
verbose: level_value(level) as u8,
|
||||
quiet: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fn level_value(level: Level) -> i8 {
|
||||
match level {
|
||||
None => -1,
|
||||
Some(log::Level::Error) => 0,
|
||||
Some(log::Level::Warn) => 1,
|
||||
Some(log::Level::Info) => 2,
|
||||
Some(log::Level::Debug) => 3,
|
||||
Some(log::Level::Trace) => 4,
|
||||
log::Level::Error => 0,
|
||||
log::Level::Warn => 1,
|
||||
log::Level::Info => 2,
|
||||
log::Level::Debug => 3,
|
||||
log::Level::Trace => 4,
|
||||
}
|
||||
}
|
||||
|
||||
const fn level_enum(verbosity: i8) -> Option<log::Level> {
|
||||
const fn level_enum(verbosity: i8) -> log::Level {
|
||||
match verbosity {
|
||||
std::i8::MIN..=-1 => None,
|
||||
0 => Some(log::Level::Error),
|
||||
1 => Some(log::Level::Warn),
|
||||
2 => Some(log::Level::Info),
|
||||
3 => Some(log::Level::Debug),
|
||||
4..=std::i8::MAX => Some(log::Level::Trace),
|
||||
0 => log::Level::Error,
|
||||
1 => log::Level::Warn,
|
||||
2 => log::Level::Info,
|
||||
3 => log::Level::Debug,
|
||||
_ => log::Level::Trace,
|
||||
}
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
impl<L: LogLevel> fmt::Display for Verbosity<L> {
|
||||
impl fmt::Display for Verbosity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.verbosity())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait LogLevel {
|
||||
fn default() -> Option<log::Level>;
|
||||
|
||||
fn verbose_help() -> Option<&'static str> {
|
||||
Some("Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)")
|
||||
}
|
||||
|
||||
fn verbose_long_help() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn quiet_help() -> Option<&'static str> {
|
||||
Some("Less output per occurrence (e.g. `-q` or `-qq`)")
|
||||
}
|
||||
|
||||
fn quiet_long_help() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct ErrorLevel;
|
||||
|
||||
impl LogLevel for ErrorLevel {
|
||||
fn default() -> Option<log::Level> {
|
||||
Some(log::Level::Error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct WarnLevel;
|
||||
|
||||
impl LogLevel for WarnLevel {
|
||||
fn default() -> Option<log::Level> {
|
||||
Some(log::Level::Warn)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct InfoLevel;
|
||||
|
||||
impl LogLevel for InfoLevel {
|
||||
fn default() -> Option<log::Level> {
|
||||
Some(log::Level::Info)
|
||||
write!(f, "{}", self.verbose)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,4 +178,11 @@ mod test {
|
|||
use clap::CommandFactory;
|
||||
Cli::command().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_log_level() {
|
||||
let verbosity = Verbosity::default();
|
||||
assert_eq!(verbosity.log_level(), Level::Warn);
|
||||
assert!(verbosity.log_level() >= Level::Warn);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::{ErrorKind, InputSource};
|
|||
/// Both, local and remote targets are supported.
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
|
||||
#[allow(variant_size_differences)]
|
||||
#[serde(try_from = "&str")]
|
||||
#[serde(try_from = "String")]
|
||||
pub enum Base {
|
||||
/// Local file path pointing to root directory
|
||||
Local(PathBuf),
|
||||
|
|
@ -72,6 +72,14 @@ impl TryFrom<&str> for Base {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Base {
|
||||
type Error = ErrorKind;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_base {
|
||||
use crate::Result;
|
||||
|
|
|
|||
Loading…
Reference in a new issue