#![allow( clippy::doc_markdown, clippy::items_after_statements, clippy::manual_assert, clippy::manual_strip, clippy::match_like_matches_macro, clippy::match_same_arms, // https://github.com/rust-lang/rust-clippy/issues/10327 clippy::module_name_repetitions, clippy::needless_pass_by_value, clippy::needless_return, clippy::new_without_default, clippy::option_option, clippy::struct_excessive_bools, clippy::too_many_lines, clippy::trivially_copy_pass_by_ref, clippy::uninlined_format_args, )] mod assets; mod cmd; mod config; mod edit; mod error; mod fmt; mod manifest; mod opts; mod unparse; mod version; use crate::cmd::CommandExt as _; use crate::config::Config; use crate::error::Result; use crate::opts::{Coloring, Expand, Subcommand}; use crate::unparse::unparse_maximal; use crate::version::Version; use bat::assets::HighlightingAssets; use bat::assets_metadata::AssetsMetadata; use bat::config::VisibleLines; use bat::line_range::{HighlightedLineRanges, LineRanges}; use bat::style::StyleComponents; use bat::theme::{ThemeName, ThemeOptions, ThemePreference}; use bat::{PagingMode, SyntaxMapping, WrappingMode}; use clap::{CommandFactory as _, Parser, ValueEnum}; use quote::quote; use std::env; use std::error::Error as StdError; use std::ffi::{OsStr, OsString}; use std::io::{self, BufRead, IsTerminal, Write}; use std::iter; use std::panic::PanicHookInfo; use std::panic::{self, UnwindSafe}; use std::path::{Path, PathBuf}; use std::process::{self, Command, Stdio}; use std::ptr; use std::str; use std::thread::Result as ThreadResult; use termcolor::{Color::Green, ColorChoice, ColorSpec, StandardStream, WriteColor}; cargo_subcommand_metadata::description!("Show result of macro expansion"); fn main() { let result = if let Some(wrapper) = env::var_os(CARGO_EXPAND_RUSTC_WRAPPER) { do_rustc_wrapper(&wrapper) } else { do_cargo_expand() }; process::exit(match result { Ok(code) => code, Err(err) => { let mut stderr = io::stderr().lock(); let _ = writeln!(stderr, "{}", err); let mut err = &err as &dyn StdError; while let Some(source) = err.source() { let _ = writeln!(stderr, "\nCaused by:\n {}", source); err = source; } 1 } }); } const CARGO_EXPAND_RUSTC_WRAPPER: &str = "CARGO_EXPAND_RUSTC_WRAPPER"; const ARG_Z_UNPRETTY_EXPANDED: &str = "-Zunpretty=expanded"; fn cargo_binary() -> OsString { env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")) } fn do_rustc_wrapper(wrapper: &OsStr) -> Result { let mut rustc_command = env::args_os().skip(1); let mut cmd = if wrapper != "/" { Command::new(wrapper) } else if let Some(rustc) = rustc_command.next() { Command::new(rustc) } else { Subcommand::command().print_help()?; return Ok(1); }; let mut is_unpretty_expanded = false; for arg in rustc_command { is_unpretty_expanded |= arg == ARG_Z_UNPRETTY_EXPANDED; cmd.arg(arg); } if is_unpretty_expanded { cmd.env("RUSTC_BOOTSTRAP", "1"); } #[cfg(unix)] { use crate::error::Error; use std::os::unix::process::CommandExt as _; let err = cmd.exec(); return Err(Error::Io(err)); } #[cfg(not(unix))] { let exit_status = cmd.status()?; let code = exit_status.code().unwrap_or(1); return Ok(code); } } fn do_cargo_expand() -> Result { let Subcommand::Expand(args) = Subcommand::parse(); if args.version { let version = Version { verbose: args.verbose, }; let _ = writeln!(io::stdout(), "{}", version); return Ok(0); } let config = config::deserialize(); if args.themes { print_themes()?; return Ok(0); } if let Some(item) = &args.item { if args.ugly { let _ = writeln!( io::stderr(), "ERROR: cannot expand single item ({}) in ugly mode.", item, ); return Ok(1); } } let mut rustfmt = None; if config.rustfmt { rustfmt = which_rustfmt(); if rustfmt.is_none() { let _ = io::stderr().write_all( b"ERROR: cargo-expand configuration sets rustfmt=true, but \ rustfmt is not found. Install rustfmt by running `rustup \ component add rustfmt --toolchain nightly`.\n", ); return Ok(1); } } let mut builder = tempfile::Builder::new(); builder.prefix("cargo-expand"); let outdir = builder.tempdir().expect("failed to create tmp file"); let outfile_path = outdir.path().join("expanded"); let color = get_color(&args, &config); // Run cargo let mut cmd = Command::new(cargo_binary()); apply_args(&mut cmd, &args, color, &outfile_path); if args.verbose { print_command(&cmd, color)?; } if needs_rustc_bootstrap() { if let Ok(current_exe) = env::current_exe() { let original_wrapper = env::var_os("RUSTC_WRAPPER").filter(|wrapper| !wrapper.is_empty()); let wrapper = original_wrapper.as_deref().unwrap_or(OsStr::new("/")); cmd.env(CARGO_EXPAND_RUSTC_WRAPPER, wrapper); cmd.env("RUSTC_WRAPPER", current_exe); } else { cmd.env("RUSTC_BOOTSTRAP", "1"); } } let code = filter_err(&mut cmd)?; if !outfile_path.exists() { return Ok(1); } let mut content = fs_err::read_to_string(&outfile_path)?; if content.is_empty() { let _ = writeln!(io::stderr(), "ERROR: rustc produced no expanded output"); return Ok(if code == 0 { 1 } else { code }); } // Format the expanded code if !args.ugly { let questionably_formatted = content; // Work around rustfmt not being able to parse paths containing $crate. // This placeholder should be the same width as $crate to preserve // alignments. const DOLLAR_CRATE_PLACEHOLDER: &str = "Ξcrate"; let wip = questionably_formatted.replace("$crate", DOLLAR_CRATE_PLACEHOLDER); enum Stage { Formatted(String), Unformatted(String), QuestionablyFormatted, } let mut stage = Stage::QuestionablyFormatted; // Discard comments, which are misplaced by the compiler if let Ok(mut syntax_tree) = syn::parse_file(&wip) { edit::sanitize(&mut syntax_tree); if let Some(filter) = args.item { syntax_tree.shebang = None; syntax_tree.attrs.clear(); syntax_tree.items = filter.apply_to(&syntax_tree); if syntax_tree.items.is_empty() { let _ = writeln!(io::stderr(), "WARNING: no such item: {}", filter); return Ok(1); } } if !config.rustfmt { if let Ok(formatted) = ignore_panic(|| unparse_maximal(&syntax_tree)) { stage = Stage::Formatted(formatted); } } if let Stage::QuestionablyFormatted = stage { let unformatted = quote!(#syntax_tree).to_string(); stage = Stage::Unformatted(unformatted); } } let to_rustfmt = match &stage { Stage::Formatted(_) => None, Stage::Unformatted(unformatted) => Some(unformatted), Stage::QuestionablyFormatted => Some(&wip), }; if let Some(unformatted) = to_rustfmt { if let Some(rustfmt) = rustfmt.or_else(which_rustfmt) { fs_err::write(&outfile_path, unformatted)?; fmt::write_rustfmt_config(&outdir)?; for edition in &["2021", "2018", "2015"] { let output = Command::new(&rustfmt) .flag_value("--edition", edition) .arg(&outfile_path) .stderr(Stdio::null()) .output(); if let Ok(output) = output { if output.status.success() { stage = Stage::Formatted(fs_err::read_to_string(&outfile_path)?); break; } } } } } content = match stage { Stage::Formatted(formatted) => formatted.replace(DOLLAR_CRATE_PLACEHOLDER, "$crate"), Stage::Unformatted(_) | Stage::QuestionablyFormatted => questionably_formatted, }; } // Run pretty printer let mut theme = args.theme.or(config.theme); let none_theme = theme.as_deref() == Some("none"); let do_color = match color { Coloring::Always => true, Coloring::Never => false, Coloring::Auto => !none_theme && io::stdout().is_terminal(), }; let _ = writeln!(io::stderr()); if do_color { let theme_result = bat::theme::theme(ThemeOptions { theme: theme .clone() .or_else(|| env::var(bat::theme::env::BAT_THEME).ok()) .map_or_else(ThemePreference::default, ThemePreference::new), theme_dark: env::var(bat::theme::env::BAT_THEME_DARK) .ok() .map(ThemeName::new), theme_light: env::var(bat::theme::env::BAT_THEME_LIGHT) .ok() .map(ThemeName::new), }); match theme_result.theme { ThemeName::Named(named) => theme = Some(named), ThemeName::Default => { if let Some(color_scheme) = theme_result.color_scheme { let default_theme = bat::theme::default_theme(color_scheme); theme = Some(default_theme.to_owned()); } } } let mut assets = HighlightingAssets::from_binary(); if let Some(requested_theme) = &theme { if !assets .themes() .any(|supported_theme| supported_theme == requested_theme) { let cache_dir = assets::cache_dir()?; if let Some(metadata) = AssetsMetadata::load_from_folder(&cache_dir)? { if metadata.is_compatible_with(assets::BAT_VERSION) { assets = HighlightingAssets::from_cache(&cache_dir)?; } } } } let config = bat::config::Config { language: Some("rust"), show_nonprintable: false, term_width: console::Term::stdout().size().1 as usize, tab_width: 4, colored_output: true, true_color: false, style_components: StyleComponents::new(&[]), wrapping_mode: WrappingMode::default(), paging_mode: if config.pager { PagingMode::QuitIfOneScreen } else { PagingMode::Never }, visible_lines: VisibleLines::Ranges(LineRanges::all()), theme: theme.unwrap_or_else(String::new), syntax_mapping: SyntaxMapping::new(), pager: None, use_italic_text: false, highlighted_lines: HighlightedLineRanges(LineRanges::none()), ..Default::default() }; let controller = bat::controller::Controller::new(&config, &assets); let inputs = vec![bat::input::Input::from_reader(Box::new(content.as_bytes()))]; // Ignore any errors. let _ = controller.run(inputs, None); } else { let _ = write!(io::stdout(), "{}", content); } Ok(0) } fn which_rustfmt() -> Option { match env::var_os("RUSTFMT") { Some(which) => { if which.is_empty() { None } else { Some(PathBuf::from(which)) } } None => toolchain_find::find_installed_component("rustfmt"), } } fn apply_args(cmd: &mut Command, args: &Expand, color: Coloring, outfile: &Path) { cmd.arg("rustc"); if args.verbose { cmd.arg("--verbose"); } if args.quiet { cmd.arg("--quiet"); } match color { Coloring::Auto => { if cfg!(not(windows)) && io::stderr().is_terminal() { cmd.flag_value("--color", "always"); } else { cmd.flag_value("--color", "never"); } } color => { cmd.flag_value("--color", color.to_possible_value().unwrap().get_name()); } } for kv in &args.config { cmd.flag_value("--config", kv); } for unstable_flag in &args.unstable_flags { cmd.arg(format!("-Z{}", unstable_flag)); } if let Some(opt_package) = &args.package { if let Some(package) = opt_package { cmd.flag_value("--package", package); } else { cmd.arg("--package"); } } let mut has_explicit_build_target = false; if args.lib { cmd.arg("--lib"); has_explicit_build_target = true; } if let Some(opt_bin) = &args.bin { if let Some(bin) = opt_bin { cmd.flag_value("--bin", bin); } else { cmd.arg("--bin"); } has_explicit_build_target = true; } if let Some(opt_example) = &args.example { if let Some(example) = opt_example { cmd.flag_value("--example", example); } else { cmd.arg("--example"); } has_explicit_build_target = true; } if let Some(opt_test) = &args.test { if let Some(test) = opt_test { cmd.flag_value("--test", test); } else { cmd.arg("--test"); } has_explicit_build_target = true; } if let Some(opt_bench) = &args.bench { if let Some(bench) = opt_bench { cmd.flag_value("--bench", bench); } else { cmd.arg("--bench"); } has_explicit_build_target = true; } if !has_explicit_build_target { if let Ok(cargo_manifest) = manifest::parse(args.manifest_path.as_deref()) { if let Some(root_package) = cargo_manifest.package { if let Some(default_run) = &root_package.default_run { cmd.flag_value("--bin", default_run); } } } } for features in &args.features { cmd.flag_value("--features", features); } if args.all_features { cmd.arg("--all-features"); } if args.no_default_features { cmd.arg("--no-default-features"); } if let Some(jobs) = args.jobs { cmd.flag_value("--jobs", jobs.to_string()); } if args.keep_going { cmd.arg("--keep-going"); } if let Some(profile) = &args.profile { cmd.flag_value("--profile", profile); } else if args.tests && args.test.is_none() { if args.release { cmd.flag_value("--profile", "bench"); } else { cmd.flag_value("--profile", "test"); } } else if args.release { cmd.flag_value("--profile", "release"); } else { cmd.flag_value("--profile", "check"); } if let Some(target) = &args.target { cmd.flag_value("--target", target); } if let Some(target_dir) = &args.target_dir { cmd.flag_value("--target-dir", target_dir); } if let Some(manifest_path) = &args.manifest_path { cmd.flag_value("--manifest-path", manifest_path); } if args.ignore_rust_version { cmd.arg("--ignore-rust-version"); } if args.locked { cmd.arg("--locked"); } if args.offline { cmd.arg("--offline"); } if args.frozen { cmd.arg("--frozen"); } cmd.arg("--"); cmd.arg("-o"); cmd.arg(outfile); cmd.arg(ARG_Z_UNPRETTY_EXPANDED); } fn needs_rustc_bootstrap() -> bool { if env::var_os("RUSTC_BOOTSTRAP").is_some_and(|var| !var.is_empty()) { return false; } let rustc = if let Some(rustc) = env::var_os("RUSTC") { PathBuf::from(rustc) } else { let mut cmd = Command::new(cargo_binary()); cmd.arg("rustc"); cmd.arg("-Zunstable-options"); cmd.flag_value("--print", "sysroot"); cmd.env("RUSTC_BOOTSTRAP", "1"); cmd.stdin(Stdio::null()); cmd.stderr(Stdio::null()); let Ok(output) = cmd.output() else { return true; }; let Ok(stdout) = str::from_utf8(&output.stdout) else { return true; }; let sysroot = Path::new(stdout.trim_end()); sysroot.join("bin").join("rustc") }; let rustc_wrapper = env::var_os("RUSTC_WRAPPER").filter(|wrapper| !wrapper.is_empty()); let rustc_workspace_wrapper = env::var_os("RUSTC_WORKSPACE_WRAPPER").filter(|wrapper| !wrapper.is_empty()); let mut wrapped_rustc = rustc_wrapper .into_iter() .chain(rustc_workspace_wrapper) .chain(iter::once(rustc.into_os_string())); let mut cmd = Command::new(wrapped_rustc.next().unwrap()); cmd.args(wrapped_rustc); cmd.arg("-Zunpretty=expanded"); cmd.arg("-"); cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); cmd.stderr(Stdio::null()); let Ok(status) = cmd.status() else { return true; }; !status.success() } fn print_command(cmd: &Command, color: Coloring) -> Result<()> { let mut shell_words = String::new(); let quoter = shlex::Quoter::new().allow_nul(true); for arg in cmd.get_args() { let arg_lossy = arg.to_string_lossy(); shell_words.push(' '); match arg_lossy.split_once('=') { Some((flag, value)) if flag.starts_with('-') && flag == quoter.quote(flag)? => { shell_words.push_str(flag); shell_words.push('='); if !value.is_empty() { shell_words.push_str("er.quote(value)?); } } _ => shell_words.push_str("er.quote(&arg_lossy)?), } } let color_choice = match color { Coloring::Auto => ColorChoice::Auto, Coloring::Always => ColorChoice::Always, Coloring::Never => ColorChoice::Never, }; let mut stream = StandardStream::stderr(color_choice); let _ = stream.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Green))); let _ = write!(stream, "{:>12}", "Running"); let _ = stream.reset(); let _ = writeln!(stream, " `cargo +nightly{}`", shell_words); Ok(()) } fn filter_err(cmd: &mut Command) -> io::Result { let mut child = cmd.stderr(Stdio::piped()).spawn()?; let mut stderr = io::BufReader::new(child.stderr.take().unwrap()); let mut line = String::new(); while let Ok(n) = stderr.read_line(&mut line) { if n == 0 { break; } if !ignore_cargo_err(&line) { let _ = write!(io::stderr(), "{}", line); } line.clear(); } let code = child.wait()?.code().unwrap_or(1); Ok(code) } fn ignore_cargo_err(line: &str) -> bool { if line.trim().is_empty() { return true; } let discarded_lines = [ "ignoring specified output filename because multiple outputs were \ requested", "ignoring specified output filename for 'link' output because multiple \ outputs were requested", "ignoring --out-dir flag due to -o flag", "ignoring -C extra-filename flag due to -o flag", "due to multiple output types requested, the explicitly specified \ output file name will be adapted for each output type", "warning emitted", "warnings emitted", ") generated ", ]; for s in &discarded_lines { if line.contains(s) { return true; } } false } fn ignore_panic(f: F) -> ThreadResult where F: UnwindSafe + FnOnce() -> T, { type PanicHook = dyn Fn(&PanicHookInfo) + Sync + Send + 'static; let null_hook: Box = Box::new(|_panic_info| { /* ignore */ }); let sanity_check = ptr::addr_of!(*null_hook); let original_hook = panic::take_hook(); panic::set_hook(null_hook); let result = panic::catch_unwind(f); let hopefully_null_hook = panic::take_hook(); panic::set_hook(original_hook); if sanity_check != &*hopefully_null_hook { panic!("race condition on std::panic hook"); } result } fn get_color(args: &Expand, config: &Config) -> Coloring { if let Some(value) = args.color { return value; } if env::var_os("NO_COLOR").is_some() { return Coloring::Never; } if let Some(value) = config.color.as_ref() { let ignore_case = false; match Coloring::from_str(value.as_str(), ignore_case) { Ok(color) => return color, Err(err) => { let _ = writeln!( io::stderr(), "WARNING: invalid color in cargo config: {}", err ); } } } Coloring::Auto // default } fn print_themes() -> Result<()> { let mut cache_dir = assets::cache_dir()?; let metadata = AssetsMetadata::load_from_folder(&cache_dir)?; let compatible = metadata .as_ref() .is_some_and(|m| m.is_compatible_with(assets::BAT_VERSION)); let assets = if compatible { HighlightingAssets::from_cache(&cache_dir)? } else { HighlightingAssets::from_binary() }; for theme in assets.themes() { let _ = writeln!(io::stdout(), "{}", theme); } if metadata.is_some() && !compatible { if let Some(home_dir) = home::home_dir() { if let Ok(relative) = cache_dir.strip_prefix(home_dir) { cache_dir = Path::new("~").join(relative); } } let bat_version = semver::Version::parse(assets::BAT_VERSION).unwrap(); let _ = writeln!( io::stderr(), "\nThere may be other themes in {cache_dir} but they are not \ compatible with the version of bat built into cargo-expand. Run \ `bat cache --build` with bat v{major}.{minor} to update the cache.", cache_dir = cache_dir.display(), major = bat_version.major, minor = bat_version.minor, ); } Ok(()) }