use anyhow::{format_err, Result};
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use std::fs;
use std::io::{stdin, stdout, Read, Write};
use std::path::{Path, PathBuf};
use structopt::{clap::arg_enum, StructOpt};
use stylua_lib::{format_code, Config};
mod output_diff;
#[derive(StructOpt, Debug)]
#[structopt(name = "stylua", about = "A utility to format Lua code")]
struct Opt {
#[structopt(long = "config-path", parse(from_os_str))]
config_path: Option<PathBuf>,
#[structopt(short, long)]
check: bool,
#[structopt(long, possible_values = &Color::variants(), case_insensitive = true, default_value = "auto")]
color: Color,
#[structopt(short, long)]
glob: Option<Vec<String>>,
#[structopt(parse(from_os_str))]
files: Vec<PathBuf>,
}
structopt::clap::arg_enum! {
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Color {
Always,
Auto,
Never,
}
}
impl Color {
pub fn use_colored_tty(self) -> bool {
match self {
Color::Always | Color::Auto => true,
Color::Never => false,
}
}
}
fn format_file(path: &Path, config: Config, check_only: bool, color: Color) -> Result<i32> {
match fs::read(path) {
Ok(contents) => {
let contents = String::from_utf8_lossy(&contents);
let formatted_contents = match format_code(&contents, config) {
Ok(formatted) => formatted,
Err(error) => {
return Err(format_err!(
"error: could not format file {}: {}",
path.display(),
error
))
}
};
if check_only {
let diff = output_diff::make_diff(&contents, &formatted_contents, 3);
if diff.is_empty() {
Ok(0)
} else {
output_diff::print_diff(
diff,
|line| format!("Diff in {} at line {}:", path.display(), line),
color,
);
Ok(1)
}
} else {
match fs::write(path, formatted_contents) {
Ok(_) => Ok(0),
Err(error) => Err(format_err!(
"error: could not write to file {}: {}",
path.display(),
error
)),
}
}
}
Err(error) => Err(format_err!(
"error: could not open file {}: {}",
path.display(),
error
)),
}
}
fn format_string(input: String, config: Config) -> Result<()> {
let out = &mut stdout();
let formatted_contents = match format_code(&input, config) {
Ok(formatted) => formatted,
Err(error) => return Err(format_err!("error: could not format from stdin: {}", error)),
};
match out.write_all(&formatted_contents.into_bytes()) {
Ok(()) => Ok(()),
Err(error) => Err(format_err!("error: could not output to stdout: {}", error)),
}
}
fn format(opt: Opt) -> Result<i32> {
if opt.files.is_empty() {
return Err(format_err!("error: no files provided"));
}
let config: Config = match opt.config_path {
Some(config_path) => match fs::read_to_string(config_path) {
Ok(contents) => match toml::from_str(&contents) {
Ok(config) => config,
Err(error) => {
return Err(format_err!(
"error: config file not in correct format: {}",
error
));
}
},
Err(error) => {
return Err(format_err!("error: couldn't read config file: {}", error));
}
},
None => match fs::read_to_string("stylua.toml") {
Ok(contents) => match toml::from_str(&contents) {
Ok(config) => config,
Err(error) => {
return Err(format_err!(
"error: config file not in correct format: {}",
error
));
}
},
Err(_) => Config::default(),
},
};
let mut errors = vec![];
let mut error_code = 0;
let cwd = std::env::current_dir()?;
let mut walker_builder = WalkBuilder::new(&opt.files[0]);
for file_path in &opt.files[1..] {
walker_builder.add(file_path);
}
walker_builder
.standard_filters(false)
.hidden(true)
.parents(true)
.add_custom_ignore_filename(".styluaignore");
let use_default_glob = match opt.glob {
Some(globs) => {
let mut overrides = OverrideBuilder::new(cwd);
for pattern in globs {
match overrides.add(&pattern) {
Ok(_) => continue,
Err(err) => errors.push(format_err!(
"error: cannot parse glob pattern {}: {}",
pattern,
err
)),
}
}
let overrides = overrides.build()?;
walker_builder.overrides(overrides);
false
}
None => true,
};
let walker = walker_builder.build();
for result in walker {
match result {
Ok(entry) => {
if entry.is_stdin() {
if opt.check {
errors.push(format_err!(
"warning: `--check` cannot be used whilst reading from stdin"
))
};
let mut buf = String::new();
match stdin().read_to_string(&mut buf) {
Ok(_) => match format_string(buf, config) {
Ok(_) => continue,
Err(error) => errors.push(error),
},
Err(error) => {
errors.push(format_err!("error: could not read from stdin: {}", error))
}
}
} else {
let path = entry.path();
if path.is_file() {
if use_default_glob {
lazy_static::lazy_static! {
static ref DEFAULT_GLOB: globset::GlobMatcher = globset::Glob::new("**/*.lua").expect("cannot create default glob").compile_matcher();
}
if !DEFAULT_GLOB.is_match(path) {
continue;
}
}
match format_file(path, config, opt.check, opt.color) {
Ok(code) => {
if code != 0 {
error_code = code
}
}
Err(error) => errors.push(error),
}
}
}
}
Err(error) => {
errors.push(format_err!("error: could not walk: {}", error));
}
}
}
if !errors.is_empty() {
for error in errors.iter() {
eprintln!("{}", error.to_string());
}
return Ok(1);
}
Ok(error_code)
}
fn main() {
let opt = Opt::from_args();
let exit_code = match format(opt) {
Ok(code) => code,
Err(e) => {
eprintln!("{}", e.to_string());
1
}
};
std::process::exit(exit_code);
}