1use anyhow::{bail, Context, Result};
20use clap::Args;
21use std::io::{Read, Write};
22use std::path::{Path, PathBuf};
23use whisker_fmt::FmtOptions;
24
25#[derive(Args, Debug)]
26pub struct FmtArgs {
27 pub files: Vec<PathBuf>,
29
30 #[arg(long)]
33 pub stdin: bool,
34
35 #[arg(long)]
38 pub check: bool,
39}
40
41pub fn run(args: FmtArgs) -> Result<()> {
42 if args.stdin {
43 return run_stdin(&args);
44 }
45 if args.files.is_empty() {
46 bail!("whisker fmt: no input files (pass file paths, or use --stdin)");
47 }
48 run_files(&args)
49}
50
51fn run_stdin(args: &FmtArgs) -> Result<()> {
54 let mut src = String::new();
55 std::io::stdin()
56 .read_to_string(&mut src)
57 .context("reading source from stdin")?;
58 let opts = resolve_options(&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
60 let formatted = whisker_fmt::format_source_in_dir(
61 &src,
62 &opts,
63 &std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
64 )
65 .context("formatting stdin")?;
66
67 if args.check {
68 if formatted != src {
69 eprint!("{}", whisker_fmt::unified_diff(&src, &formatted));
70 std::process::exit(1);
71 }
72 return Ok(());
73 }
74
75 let mut stdout = std::io::stdout().lock();
76 stdout
77 .write_all(formatted.as_bytes())
78 .context("writing formatted output to stdout")?;
79 Ok(())
80}
81
82fn run_files(args: &FmtArgs) -> Result<()> {
83 let mut any_changed = false;
84 let mut errored = false;
85
86 for file in &args.files {
87 match format_one_file(file, args.check) {
88 Ok(changed) => {
89 if changed {
90 any_changed = true;
91 if args.check {
92 } else {
94 eprintln!("formatted {}", file.display());
95 }
96 }
97 }
98 Err(e) => {
99 errored = true;
100 eprintln!("error: {}: {e:#}", file.display());
101 }
102 }
103 }
104
105 if errored {
106 std::process::exit(1);
107 }
108 if args.check && any_changed {
109 std::process::exit(1);
110 }
111 Ok(())
112}
113
114fn format_one_file(path: &Path, check: bool) -> Result<bool> {
118 let src =
119 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
120 let dir = match path.parent() {
123 Some(p) if !p.as_os_str().is_empty() => p,
124 _ => Path::new("."),
125 };
126 let opts = resolve_options(dir);
127
128 let formatted = whisker_fmt::format_source_in_dir(&src, &opts, dir)
129 .with_context(|| format!("formatting {}", path.display()))?;
130
131 if formatted == src {
132 return Ok(false);
133 }
134
135 if check {
136 println!("Diff in {}:", path.display());
137 print!("{}", whisker_fmt::unified_diff(&src, &formatted));
138 } else {
139 std::fs::write(path, &formatted).with_context(|| format!("writing {}", path.display()))?;
140 }
141 Ok(true)
142}
143
144fn resolve_options(dir: &Path) -> FmtOptions {
153 whisker_fmt::resolve_options(dir)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn resolve_options_defaults_without_toml() {
162 let tmp = std::env::temp_dir().join(format!("whisker-fmt-test-{}", std::process::id()));
166 let _ = std::fs::create_dir_all(&tmp);
167 let o = resolve_options(&tmp);
168 assert!(o.tab_spaces >= 1);
171 let _ = std::fs::remove_dir_all(&tmp);
172 }
173
174 #[test]
175 fn resolve_options_reads_local_toml() {
176 let tmp = std::env::temp_dir().join(format!(
177 "whisker-fmt-test-toml-{}-{}",
178 std::process::id(),
179 std::time::SystemTime::now()
180 .duration_since(std::time::UNIX_EPOCH)
181 .unwrap()
182 .as_nanos()
183 ));
184 std::fs::create_dir_all(&tmp).unwrap();
185 std::fs::write(tmp.join("rustfmt.toml"), "tab_spaces = 2\nmax_width = 80\n").unwrap();
186 let o = resolve_options(&tmp);
187 assert_eq!(o.tab_spaces, 2);
188 assert_eq!(o.max_width, 80);
189 std::fs::remove_dir_all(&tmp).unwrap();
190 }
191}