fb2-clean 0.2.1

A CLI utility for clean fb2 files.
Documentation
use super::{Config, Input, Output, Tags};
use clap::{
    Arg, ArgAction, ArgMatches, Command, CommandFactory, Error, FromArgMatches, Parser,
    builder::{TypedValueParser, ValueParser},
    error::{ContextKind, ContextValue, ErrorKind},
    value_parser,
};
use std::ffi::OsStr;

type Result<T> = std::result::Result<T, clap::Error>;

impl Parser for Config {}

impl FromArgMatches for Config {
    fn from_arg_matches(m: &ArgMatches) -> Result<Config> {
        Self::from_arg_matches_mut(&mut m.clone())
    }

    fn update_from_arg_matches(&mut self, m: &ArgMatches) -> Result<()> {
        self.update_from_arg_matches_mut(&mut m.clone())
    }

    fn from_arg_matches_mut(m: &mut ArgMatches) -> Result<Config> {
        let input = match m.remove_one::<Input>("input") {
            Some(i) => i,
            None => Input::new(".").unwrap(),
        };
        let output = match m.remove_one::<Output>("output") {
            Some(o) => o,
            None => Output::try_from_input(&input).unwrap(),
        };

        Ok(Config {
            input,
            output,
            recursive: *m.get_one::<u8>("recursive").unwrap_or(&0),
            tags: m
                .remove_one::<Tags>("tags")
                .unwrap_or_else(|| Tags::default()),
            zip: m.get_flag("zip"),
            unzip: m.get_flag("unzip"),
            force: m.get_flag("force"),
            exit_on_err: m.get_flag("exit-on-err"),
        })
    }

    fn update_from_arg_matches_mut(&mut self, m: &mut ArgMatches) -> Result<()> {
        *self = Self::from_arg_matches_mut(m)?;
        Ok(())
    }
}

impl CommandFactory for Config {
    fn command() -> Command {
        Command::new(env!("CARGO_PKG_NAME"))
            .version(concat!("v", env!("CARGO_PKG_VERSION")))
            .disable_help_flag(true)
            .disable_version_flag(true)
            .override_usage(concat!(env!("CARGO_PKG_NAME"), " [options]"))
            .arg(
                Arg::new("input")
                    .short('i')
                    .long("input")
                    .value_name("path")
                    .help("Input directory OR file")
                    .value_parser(ValueParser::new(InputParser)),
            )
            .arg(
                Arg::new("output")
                    .short('o')
                    .long("output")
                    .value_name("dir")
                    .help("Output directory")
                    .value_parser(ValueParser::new(OutputParser)),
            )
            .arg(
                Arg::new("recursive")
                    .short('r')
                    .long("recursive")
                    .value_name("n")
                    .help("Recursive file search [up to n]")
                    .num_args(0..=1)
                    .default_missing_value("16")
                    .value_parser(value_parser!(u8).range(1..)),
            )
            .arg(
                Arg::new("tags")
                    .short('t')
                    .long("tags")
                    .value_name("n[,m...]")
                    .help("Remove tags")
                    .value_parser(ValueParser::new(TagsParser)),
            )
            .arg(
                Arg::new("zip")
                    .short('z')
                    .long("zip")
                    .help("Compress fb2 to fb2.zip")
                    .action(ArgAction::SetTrue),
            )
            .arg(
                Arg::new("unzip")
                    .short('Z')
                    .long("unzip")
                    .alias("no-zip")
                    .help("Uncompress fb2.zip to fb2")
                    .conflicts_with("zip")
                    .action(ArgAction::SetTrue),
            )
            .arg(
                Arg::new("force")
                    .short('f')
                    .long("force")
                    .help("Force overwrite input files")
                    .action(ArgAction::SetTrue),
            )
            .arg(
                Arg::new("exit-on-err")
                    .short('e')
                    .long("exit-on-err")
                    .alias("exit-on-error")
                    .help("Skip clean next files on error")
                    .action(ArgAction::SetTrue),
            )
            .next_help_heading("Other")
            .arg(
                Arg::new("version")
                    .short('V')
                    .long("version")
                    .help("Show version")
                    .action(ArgAction::Version),
            )
            .arg(
                Arg::new("help")
                    .short('h')
                    .long("help")
                    .help("Show help")
                    .action(ArgAction::Help),
            )
    }

    fn command_for_update() -> Command {
        Self::command()
    }
}

macro_rules! ty_parser {
    ($parser:ident, $ty:ty, $try_from_os_str:path) => {
        #[derive(Clone)]
        pub(super) struct $parser;

        impl TypedValueParser for $parser {
            type Value = $ty;

            fn parse_ref(
                &self,
                cmd: &Command,
                arg: Option<&Arg>,
                value: &OsStr,
            ) -> Result<Self::Value> {
                $try_from_os_str(value).map_err(|e| {
                    let mut err = Error::new(ErrorKind::InvalidValue).with_cmd(&cmd);

                    if let Some(arg) = arg {
                        err.insert(
                            ContextKind::InvalidArg,
                            ContextValue::String(arg.to_string()),
                        );
                    }
                    err.insert(
                        ContextKind::InvalidValue,
                        ContextValue::String(format!(
                            "'{}' (reason: {})",
                            value.to_string_lossy(),
                            e
                        )),
                    );

                    err
                })
            }
        }
    };
}

ty_parser!(InputParser, Input, Input::new);
ty_parser!(OutputParser, Output, Output::new);
ty_parser!(TagsParser, Tags, Tags::fallible_new);