#![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<i32> {
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<i32> {
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);
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 });
}
if !args.ugly {
let questionably_formatted = content;
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;
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,
};
}
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()))];
let _ = controller.run(inputs, None);
} else {
let _ = write!(io::stdout(), "{}", content);
}
Ok(0)
}
fn which_rustfmt() -> Option<PathBuf> {
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");
}
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);
}
}
}
}
if let Some(features) = &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 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.frozen {
cmd.arg("--frozen");
}
if args.locked {
cmd.arg("--locked");
}
if args.offline {
cmd.arg("--offline");
}
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<i32> {
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, T>(f: F) -> ThreadResult<T>
where
F: UnwindSafe + FnOnce() -> T,
{
type PanicHook = dyn Fn(&PanicHookInfo) + Sync + Send + 'static;
let null_hook: Box<PanicHook> = Box::new(|_panic_info| { });
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 }
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(())
}