cargo-sync-rdme 0.5.0

Cargo subcommand to synchronize README with crate documentation
Documentation
//! Cargo subcommand to synchronize README with the cargo manifest and crate
//! documentation.
//!
//! See [repository's README] for `cargo-sync-rdme` command usage.
//!
//! # Usage
//!
//! Add this to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! cargo-sync-rdme = "0.5.0"
//! ```
//!
//! [repository's README]: https://github.com/gifnksm/cargo-sync-rdme/blob/main/README.md
#![doc(html_root_url = "https://docs.rs/cargo-sync-rdme/0.5.0")]
#![warn(
    elided_lifetimes_in_paths,
    explicit_outlives_requirements,
    keyword_idents,
    missing_copy_implementations,
    missing_debug_implementations,
    missing_docs,
    single_use_lifetimes,
    unreachable_pub,
    unused
)]

use std::{env, io, process};

use clap::{CommandFactory as _, Parser};
use clap_complete::{Generator, Shell};
use tracing::Level;
use tracing_subscriber::{EnvFilter, filter::LevelFilter};

#[macro_use]
mod macros;

mod cli;
mod config;
mod diff;
mod sync;
mod traits;
mod vcs;
mod with_source;

pub use self::cli::App;

/// Error type for `cargo-sync-rdme` command.
pub type Error = miette::Error;

/// Result type for `cargo-sync-rdme` command.
pub type Result<T> = miette::Result<T>;

/// Entry point of `cargo-sync-rdme` command.
pub fn main(bin_name: &str) -> Result<()> {
    let env_prefix = bin_name.to_uppercase().replace("-", "_");
    if let Ok(shell) = env::var(format!("{env_prefix}_PRINT_COMPLETION")) {
        print_completion(bin_name, &shell);
        process::exit(0);
    }
    if let Ok(output_dir) = env::var(format!("{env_prefix}_GENERATE_MAN_TO")) {
        generate_man(&output_dir);
        process::exit(0);
    }

    // If this command is run by cargo, the first argument is the subcommand name
    // `sync-rdme`. We need to remove it to avoid parsing error.
    let args = env::args().enumerate().filter_map(|(idx, arg)| {
        if idx == 1 && arg == "sync-rdme" {
            None
        } else {
            Some(arg)
        }
    });
    let app = App::parse_from(args);
    install_logger(app.verbosity.into())?;

    let workspace = app.workspace.metadata()?;
    for package in app.package.packages(&workspace)? {
        sync::sync_all(&app, &workspace, package)?;
    }

    Ok(())
}

fn install_logger(verbosity: Option<Level>) -> Result<()> {
    let env_filter = if env::var_os("RUST_LOG").is_some() {
        EnvFilter::from_default_env()
    } else {
        let default_level = match verbosity {
            Some(Level::ERROR) => LevelFilter::ERROR,
            Some(Level::WARN) => LevelFilter::WARN,
            Some(Level::INFO) => LevelFilter::INFO,
            Some(Level::DEBUG) => LevelFilter::DEBUG,
            Some(Level::TRACE) => LevelFilter::TRACE,
            None => LevelFilter::OFF,
        };
        EnvFilter::builder()
            .with_default_directive(default_level.into())
            .from_env_lossy()
    };

    tracing_subscriber::fmt()
        .with_env_filter(env_filter)
        .with_writer(io::stderr)
        .with_target(false)
        .try_init()
        .map_err(|e| miette!(e))?;

    Ok(())
}

fn print_completion(bin_name: &str, shell: &str) {
    fn print<G>(bin_name: &str, g: G)
    where
        G: Generator,
    {
        clap_complete::generate(g, &mut App::command(), bin_name, &mut io::stdout());
    }
    match shell {
        "bash" => print(bin_name, Shell::Bash),
        "elvish" => print(bin_name, Shell::Elvish),
        "fish" => print(bin_name, Shell::Fish),
        "powershell" => print(bin_name, Shell::PowerShell),
        "zsh" => print(bin_name, Shell::Zsh),
        "nushell" => print(bin_name, clap_complete_nushell::Nushell),
        _ => panic!(
            "error: unknown shell `{shell}`, expected one of `bash`, `elvish`, `fish`, `powershell`, `zsh`, `nushell`"
        ),
    }
}

fn generate_man(output_dir: &str) {
    clap_mangen::generate_to(App::command(), output_dir).unwrap();
}