diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..99ae605a6d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,150 @@ +//! The mdbook CLI library + +#![allow(unreachable_pub, reason = "not needed in a bin crate")] + +use anyhow::anyhow; +use clap::{Arg, ArgMatches, Command}; +use clap_complete::Shell; +use mdbook_core::utils; +use std::env; +use std::ffi::OsStr; +use std::path::PathBuf; +use tracing::{error, info}; + +mod cmd; + +const VERSION: &str = concat!("v", clap::crate_version!()); + +/// The main entrypoint +pub fn main() { + init_logger(); + + let command = create_clap_command(); + + // Check which subcommand the user ran... + let res = match command.get_matches().subcommand() { + Some(("init", sub_matches)) => cmd::init::execute(sub_matches), + Some(("build", sub_matches)) => cmd::build::execute(sub_matches), + Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches), + #[cfg(feature = "watch")] + Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches), + #[cfg(feature = "serve")] + Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), + Some(("test", sub_matches)) => cmd::test::execute(sub_matches), + Some(("completions", sub_matches)) => (|| { + let shell = sub_matches + .get_one::("shell") + .ok_or_else(|| anyhow!("Shell name missing."))?; + + let mut complete_app = create_clap_command(); + clap_complete::generate( + *shell, + &mut complete_app, + "mdbook", + &mut std::io::stdout().lock(), + ); + Ok(()) + })(), + _ => unreachable!(), + }; + + if let Err(e) = res { + utils::log_backtrace(&e); + + std::process::exit(101); + } +} + +/// Create a list of valid arguments and sub-commands +fn create_clap_command() -> Command { + let app = Command::new(clap::crate_name!()) + .about(clap::crate_description!()) + .author("Mathieu David ") + .version(VERSION) + .propagate_version(true) + .arg_required_else_help(true) + .after_help( + "For more information about a specific command, try `mdbook --help`\n\ + The source code for mdBook is available at: https://github.com/rust-lang/mdBook", + ) + .subcommand(cmd::init::make_subcommand()) + .subcommand(cmd::build::make_subcommand()) + .subcommand(cmd::test::make_subcommand()) + .subcommand(cmd::clean::make_subcommand()) + .subcommand( + Command::new("completions") + .about("Generate shell completions for your shell to stdout") + .arg( + Arg::new("shell") + .value_parser(clap::value_parser!(Shell)) + .help("the shell to generate completions for") + .value_name("SHELL") + .required(true), + ), + ); + + #[cfg(feature = "watch")] + let app = app.subcommand(cmd::watch::make_subcommand()); + #[cfg(feature = "serve")] + let app = app.subcommand(cmd::serve::make_subcommand()); + + app +} + +fn init_logger() { + let filter = tracing_subscriber::EnvFilter::builder() + .with_env_var("MDBOOK_LOG") + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(); + let log_env = std::env::var("MDBOOK_LOG"); + // Silence some particularly noisy dependencies unless the user + // specifically asks for them. + let silence_unless_specified = |filter: tracing_subscriber::EnvFilter, target| { + if !log_env.as_ref().map_or(false, |s| { + s.split(',').any(|directive| directive.starts_with(target)) + }) { + filter.add_directive(format!("{target}=warn").parse().unwrap()) + } else { + filter + } + }; + let filter = silence_unless_specified(filter, "handlebars"); + let filter = silence_unless_specified(filter, "html5ever"); + + // Don't show the target by default, since it generally isn't useful + // unless you are overriding the level. + let with_target = log_env.is_ok(); + + tracing_subscriber::fmt() + .without_time() + .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr())) + .with_writer(std::io::stderr) + .with_env_filter(filter) + .with_target(with_target) + .init(); +} + +fn get_book_dir(args: &ArgMatches) -> PathBuf { + if let Some(p) = args.get_one::("dir") { + // Check if path is relative from current dir, or absolute... + if p.is_relative() { + env::current_dir().unwrap().join(p) + } else { + p.to_path_buf() + } + } else { + env::current_dir().expect("Unable to determine the current directory") + } +} + +fn open>(path: P) { + info!("Opening web browser"); + if let Err(e) = opener::open(path) { + error!("Error opening web browser: {}", e); + } +} + +#[test] +fn verify_app() { + create_clap_command().debug_assert(); +} diff --git a/src/main.rs b/src/main.rs index 68fea9c49c..b12056b8a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,149 +1,9 @@ //! The mdbook CLI. -#![allow(unreachable_pub, reason = "not needed in a bin crate")] - -use anyhow::anyhow; -use clap::{Arg, ArgMatches, Command}; -use clap_complete::Shell; -use mdbook_core::utils; -use std::env; -use std::ffi::OsStr; -use std::path::PathBuf; -use tracing::{error, info}; - -mod cmd; - -const VERSION: &str = concat!("v", clap::crate_version!()); - fn main() { - init_logger(); - - let command = create_clap_command(); - - // Check which subcommand the user ran... - let res = match command.get_matches().subcommand() { - Some(("init", sub_matches)) => cmd::init::execute(sub_matches), - Some(("build", sub_matches)) => cmd::build::execute(sub_matches), - Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches), - #[cfg(feature = "watch")] - Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches), - #[cfg(feature = "serve")] - Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), - Some(("test", sub_matches)) => cmd::test::execute(sub_matches), - Some(("completions", sub_matches)) => (|| { - let shell = sub_matches - .get_one::("shell") - .ok_or_else(|| anyhow!("Shell name missing."))?; - - let mut complete_app = create_clap_command(); - clap_complete::generate( - *shell, - &mut complete_app, - "mdbook", - &mut std::io::stdout().lock(), - ); - Ok(()) - })(), - _ => unreachable!(), - }; - - if let Err(e) = res { - utils::log_backtrace(&e); - - std::process::exit(101); - } -} - -/// Create a list of valid arguments and sub-commands -fn create_clap_command() -> Command { - let app = Command::new(clap::crate_name!()) - .about(clap::crate_description!()) - .author("Mathieu David ") - .version(VERSION) - .propagate_version(true) - .arg_required_else_help(true) - .after_help( - "For more information about a specific command, try `mdbook --help`\n\ - The source code for mdBook is available at: https://github.com/rust-lang/mdBook", - ) - .subcommand(cmd::init::make_subcommand()) - .subcommand(cmd::build::make_subcommand()) - .subcommand(cmd::test::make_subcommand()) - .subcommand(cmd::clean::make_subcommand()) - .subcommand( - Command::new("completions") - .about("Generate shell completions for your shell to stdout") - .arg( - Arg::new("shell") - .value_parser(clap::value_parser!(Shell)) - .help("the shell to generate completions for") - .value_name("SHELL") - .required(true), - ), - ); - - #[cfg(feature = "watch")] - let app = app.subcommand(cmd::watch::make_subcommand()); - #[cfg(feature = "serve")] - let app = app.subcommand(cmd::serve::make_subcommand()); - - app -} - -fn init_logger() { - let filter = tracing_subscriber::EnvFilter::builder() - .with_env_var("MDBOOK_LOG") - .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) - .from_env_lossy(); - let log_env = std::env::var("MDBOOK_LOG"); - // Silence some particularly noisy dependencies unless the user - // specifically asks for them. - let silence_unless_specified = |filter: tracing_subscriber::EnvFilter, target| { - if !log_env.as_ref().map_or(false, |s| { - s.split(',').any(|directive| directive.starts_with(target)) - }) { - filter.add_directive(format!("{target}=warn").parse().unwrap()) - } else { - filter - } - }; - let filter = silence_unless_specified(filter, "handlebars"); - let filter = silence_unless_specified(filter, "html5ever"); - - // Don't show the target by default, since it generally isn't useful - // unless you are overriding the level. - let with_target = log_env.is_ok(); - - tracing_subscriber::fmt() - .without_time() - .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr())) - .with_writer(std::io::stderr) - .with_env_filter(filter) - .with_target(with_target) - .init(); -} - -fn get_book_dir(args: &ArgMatches) -> PathBuf { - if let Some(p) = args.get_one::("dir") { - // Check if path is relative from current dir, or absolute... - if p.is_relative() { - env::current_dir().unwrap().join(p) - } else { - p.to_path_buf() - } - } else { - env::current_dir().expect("Unable to determine the current directory") - } -} - -fn open>(path: P) { - info!("Opening web browser"); - if let Err(e) = opener::open(path) { - error!("Error opening web browser: {}", e); - } -} - -#[test] -fn verify_app() { - create_clap_command().debug_assert(); + // Note that `main` lives in a library to allow consumers + // to access the mdbook entrypoint without the need for + // a nightly toolchain to use `-Z bindeps` + // https://github.com/rust-lang/cargo/issues/9096 + mdbook::main() }