use anyhow::{bail, Context, Result};
use clap::Args;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use whisker_fmt::FmtOptions;
#[derive(Args, Debug)]
pub struct FmtArgs {
pub files: Vec<PathBuf>,
#[arg(long)]
pub stdin: bool,
#[arg(long)]
pub check: bool,
}
pub fn run(args: FmtArgs) -> Result<()> {
if args.stdin {
return run_stdin(&args);
}
if args.files.is_empty() {
bail!("whisker fmt: no input files (pass file paths, or use --stdin)");
}
run_files(&args)
}
fn run_stdin(args: &FmtArgs) -> Result<()> {
let mut src = String::new();
std::io::stdin()
.read_to_string(&mut src)
.context("reading source from stdin")?;
let opts = resolve_options(&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let formatted = whisker_fmt::format_source_in_dir(
&src,
&opts,
&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
)
.context("formatting stdin")?;
if args.check {
if formatted != src {
eprint!("{}", whisker_fmt::unified_diff(&src, &formatted));
std::process::exit(1);
}
return Ok(());
}
let mut stdout = std::io::stdout().lock();
stdout
.write_all(formatted.as_bytes())
.context("writing formatted output to stdout")?;
Ok(())
}
fn run_files(args: &FmtArgs) -> Result<()> {
let mut any_changed = false;
let mut errored = false;
for file in &args.files {
match format_one_file(file, args.check) {
Ok(changed) => {
if changed {
any_changed = true;
if args.check {
} else {
eprintln!("formatted {}", file.display());
}
}
}
Err(e) => {
errored = true;
eprintln!("error: {}: {e:#}", file.display());
}
}
}
if errored {
std::process::exit(1);
}
if args.check && any_changed {
std::process::exit(1);
}
Ok(())
}
fn format_one_file(path: &Path, check: bool) -> Result<bool> {
let src =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let dir = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
let opts = resolve_options(dir);
let formatted = whisker_fmt::format_source_in_dir(&src, &opts, dir)
.with_context(|| format!("formatting {}", path.display()))?;
if formatted == src {
return Ok(false);
}
if check {
println!("Diff in {}:", path.display());
print!("{}", whisker_fmt::unified_diff(&src, &formatted));
} else {
std::fs::write(path, &formatted).with_context(|| format!("writing {}", path.display()))?;
}
Ok(true)
}
fn resolve_options(dir: &Path) -> FmtOptions {
whisker_fmt::resolve_options(dir)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_options_defaults_without_toml() {
let tmp = std::env::temp_dir().join(format!("whisker-fmt-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp);
let o = resolve_options(&tmp);
assert!(o.tab_spaces >= 1);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn resolve_options_reads_local_toml() {
let tmp = std::env::temp_dir().join(format!(
"whisker-fmt-test-toml-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("rustfmt.toml"), "tab_spaces = 2\nmax_width = 80\n").unwrap();
let o = resolve_options(&tmp);
assert_eq!(o.tab_spaces, 2);
assert_eq!(o.max_width, 80);
std::fs::remove_dir_all(&tmp).unwrap();
}
}