use std::io::Read;
use std::path::PathBuf;
#[derive(Clone)]
pub struct Indent(String);
impl Indent {
pub fn parse(spec: &str) -> Result<Self, String> {
if spec.eq_ignore_ascii_case("tab") {
return Ok(Self("\t".into()));
}
match spec.parse::<usize>() {
Ok(n) if (1..=16).contains(&n) => Ok(Self(" ".repeat(n))),
_ => Err(format!("bad indent `{spec}` (expected 1 to 16, or `tab`)")),
}
}
fn as_str(&self) -> &str {
&self.0
}
}
impl Default for Indent {
fn default() -> Self {
Self(" ".into())
}
}
pub struct Options {
pub paths: Vec<PathBuf>,
pub indent: Indent,
pub check: bool,
pub to_stdout: bool,
pub stdin: bool,
}
pub fn run(options: Options) -> i32 {
if options.stdin {
return format_stdin(&options);
}
let files = match crate::files::collect(&options.paths, crate::files::Components::Include) {
Ok(files) => files,
Err(err) => {
eprintln!("rux: {err}");
return 2;
}
};
if files.is_empty() {
eprintln!("rux: no .rux files found");
return 2;
}
if options.to_stdout && files.len() > 1 {
eprintln!("rux: --stdout takes a single file, got {}", files.len());
return 2;
}
let mut changed = Vec::new();
let mut failed = false;
for file in &files {
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("rux: reading {}: {e}", file.display());
failed = true;
continue;
}
};
let formatted = rux_fmt::reindent(&source, options.indent.as_str());
if options.to_stdout {
print!("{formatted}");
continue;
}
if formatted == source {
continue;
}
changed.push(file.clone());
if options.check {
continue;
}
if let Err(e) = std::fs::write(file, &formatted) {
eprintln!("rux: writing {}: {e}", file.display());
failed = true;
}
}
if failed {
return 2;
}
if options.to_stdout {
return 0;
}
report(&changed, files.len(), options.check);
if options.check && !changed.is_empty() {
1
} else {
0
}
}
fn report(changed: &[PathBuf], total: usize, check: bool) {
let files = if total == 1 { "file" } else { "files" };
if changed.is_empty() {
eprintln!("rux: {total} {files} already formatted");
return;
}
for path in changed {
if check {
println!("{}", path.display());
}
}
let n = changed.len();
if check {
eprintln!("rux: {n} of {total} {files} would be reformatted");
} else {
eprintln!("rux: reformatted {n} of {total} {files}");
}
}
fn format_stdin(options: &Options) -> i32 {
let mut source = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut source) {
eprintln!("rux: reading stdin: {e}");
return 2;
}
let formatted = rux_fmt::reindent(&source, options.indent.as_str());
if options.check && formatted != source {
return 1;
}
print!("{formatted}");
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indent_accepts_spaces_and_tabs() {
assert_eq!(Indent::parse("4").unwrap().as_str(), " ");
assert_eq!(Indent::parse("tab").unwrap().as_str(), "\t");
assert_eq!(Indent::parse("TAB").unwrap().as_str(), "\t");
assert_eq!(Indent::default().as_str(), " ");
}
#[test]
fn indent_rejects_nonsense() {
assert!(Indent::parse("0").is_err());
assert!(Indent::parse("-2").is_err());
assert!(Indent::parse("999").is_err());
assert!(Indent::parse("two").is_err());
}
}