use std::{
io::{self, Write},
num::NonZeroU64,
ops::Deref,
path::{self, PathBuf},
str::FromStr,
};
use anyhow::bail;
use clap::{CommandFactory, Parser, ValueEnum, ValueHint};
use clap_complete::Generator;
use simplelog::LevelFilter;
#[derive(Debug, Parser)]
#[allow(clippy::struct_excessive_bools)]
#[command(version, about, max_term_width(100))]
pub struct Opt {
#[arg(short('c'), long, conflicts_with("remove"), conflicts_with("suffix"))]
pub stdout: bool,
#[arg(short, long)]
pub force: bool,
#[arg(short, long, conflicts_with("remove"))]
pub _keep: bool,
#[arg(long("rm"))]
pub remove: bool,
#[arg(short('S'), long)]
pub suffix: Option<Suffix>,
#[arg(short, long, default_value("15"), value_name("TIMES"))]
pub iteration: NonZeroU64,
#[arg(long, value_enum, default_value_t, ignore_case(true))]
pub format: Format,
#[arg(
long,
value_enum,
default_value_t,
value_name("LEVEL"),
ignore_case(true)
)]
pub log_level: LogLevel,
#[arg(long, value_enum, value_name("SHELL"))]
pub generate_completion: Option<Shell>,
#[arg(value_name("FILE"), value_hint(ValueHint::FilePath))]
pub input: Option<Vec<PathBuf>>,
}
impl Opt {
pub fn print_completion(generator: impl Generator) {
clap_complete::generate(
generator,
&mut Self::command(),
Self::command().get_name(),
&mut io::stdout(),
);
}
}
#[derive(Clone, Debug, ValueEnum)]
#[allow(clippy::doc_markdown)]
#[value(rename_all = "lower")]
pub enum Shell {
Bash,
Elvish,
Fish,
Nushell,
#[allow(clippy::enum_variant_names)]
PowerShell,
Zsh,
}
impl Generator for Shell {
fn file_name(&self, name: &str) -> String {
match self {
Self::Bash => clap_complete::Shell::Bash.file_name(name),
Self::Elvish => clap_complete::Shell::Elvish.file_name(name),
Self::Fish => clap_complete::Shell::Fish.file_name(name),
Self::Nushell => clap_complete_nushell::Nushell.file_name(name),
Self::PowerShell => clap_complete::Shell::PowerShell.file_name(name),
Self::Zsh => clap_complete::Shell::Zsh.file_name(name),
}
}
fn generate(&self, cmd: &clap::Command, buf: &mut dyn Write) {
match self {
Self::Bash => clap_complete::Shell::Bash.generate(cmd, buf),
Self::Elvish => clap_complete::Shell::Elvish.generate(cmd, buf),
Self::Fish => clap_complete::Shell::Fish.generate(cmd, buf),
Self::Nushell => clap_complete_nushell::Nushell.generate(cmd, buf),
Self::PowerShell => clap_complete::Shell::PowerShell.generate(cmd, buf),
Self::Zsh => clap_complete::Shell::Zsh.generate(cmd, buf),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Suffix(String);
impl Deref for Suffix {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for Suffix {
type Err = anyhow::Error;
fn from_str(suffix: &str) -> anyhow::Result<Self> {
if suffix.is_empty() {
bail!("the suffix is an empty string");
}
if suffix.contains(path::MAIN_SEPARATOR) {
bail!("the suffix contains a path separator");
}
if !suffix.starts_with('.') {
bail!("the suffix does not starts with `.`");
}
Ok(Self(suffix.into()))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum Format {
#[default]
Gzip,
Zlib,
Deflate,
}
impl From<Format> for zopfli::Format {
fn from(format: Format) -> Self {
match format {
Format::Gzip => Self::Gzip,
Format::Zlib => Self::Zlib,
Format::Deflate => Self::Deflate,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "UPPER")]
pub enum LogLevel {
Off,
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
impl From<LogLevel> for LevelFilter {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Off => Self::Off,
LogLevel::Error => Self::Error,
LogLevel::Warn => Self::Warn,
LogLevel::Info => Self::Info,
LogLevel::Debug => Self::Debug,
LogLevel::Trace => Self::Trace,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_app() {
Opt::command().debug_assert();
}
#[test]
fn file_name_shell() {
assert_eq!(Shell::Bash.file_name("rzopfli"), "rzopfli.bash");
assert_eq!(Shell::Elvish.file_name("rzopfli"), "rzopfli.elv");
assert_eq!(Shell::Fish.file_name("rzopfli"), "rzopfli.fish");
assert_eq!(Shell::Nushell.file_name("rzopfli"), "rzopfli.nu");
assert_eq!(Shell::PowerShell.file_name("rzopfli"), "_rzopfli.ps1");
assert_eq!(Shell::Zsh.file_name("rzopfli"), "_rzopfli");
}
#[test]
fn deref_suffix() {
assert_eq!(&*Suffix(String::default()), "");
}
#[test]
fn from_str_suffix() {
assert_eq!(Suffix::from_str(".gz").unwrap(), Suffix(".gz".into()));
}
#[test]
fn from_str_suffix_with_empty_string() {
assert!(
Suffix::from_str("")
.unwrap_err()
.to_string()
.contains("the suffix is an empty string")
);
}
#[test]
fn from_str_suffix_with_path_separator() {
let suffix = if cfg!(windows) { r"foo\bar" } else { "foo/bar" };
assert!(
Suffix::from_str(suffix)
.unwrap_err()
.to_string()
.contains("the suffix contains a path separator")
);
}
#[test]
fn from_str_suffix_not_starts_with_dot() {
assert!(
Suffix::from_str("gz")
.unwrap_err()
.to_string()
.contains("the suffix does not starts with `.`")
);
}
#[test]
fn default_format() {
assert_eq!(Format::default(), Format::Gzip);
}
#[test]
fn from_format_to_zopfli_format() {
assert!(matches!(
zopfli::Format::from(Format::Gzip),
zopfli::Format::Gzip
));
assert!(matches!(
zopfli::Format::from(Format::Zlib),
zopfli::Format::Zlib
));
assert!(matches!(
zopfli::Format::from(Format::Deflate),
zopfli::Format::Deflate
));
}
#[test]
fn default_log_level() {
assert_eq!(LogLevel::default(), LogLevel::Info);
}
#[test]
fn from_log_level_to_level_filter() {
assert_eq!(LevelFilter::from(LogLevel::Off), LevelFilter::Off);
assert_eq!(LevelFilter::from(LogLevel::Error), LevelFilter::Error);
assert_eq!(LevelFilter::from(LogLevel::Warn), LevelFilter::Warn);
assert_eq!(LevelFilter::from(LogLevel::Info), LevelFilter::Info);
assert_eq!(LevelFilter::from(LogLevel::Debug), LevelFilter::Debug);
assert_eq!(LevelFilter::from(LogLevel::Trace), LevelFilter::Trace);
}
}