1
Fork 0
mirror of https://github.com/RGBCube/alejandra synced 2025-07-30 12:07:46 +00:00

feat: exit codes as public api

This commit is contained in:
Kevin Amado 2022-02-25 15:38:56 -05:00
parent 424e300ab5
commit 391e33c537
7 changed files with 177 additions and 94 deletions

View file

@ -195,6 +195,7 @@ Our public API consists of:
- The CLI tool (`$ alejandra`),
command line flags,
positional arguments,
exit codes,
and stdout.
## Changelog

View file

@ -33,6 +33,10 @@ steps:
- formatting-before-vs-after.patch.txt
- formatting-after.patch.txt
command:
- echo +++ Formatting - demo
- nix run . -- flake.nix
- echo --- Cloning nixpkgs
- git config --global user.email CI/CD
- git config --global user.name CI/CD
- git clone --branch=master --depth 1 --origin=upstream file:///data/nixpkgs

View file

@ -12,5 +12,5 @@ pub fn main() -> Result<(), JsValue> {
#[wasm_bindgen]
pub fn format(before: String, path: String) -> String {
alejandra_engine::format::string_or_passthrough(path, before)
alejandra_engine::format::in_memory(path, before).1
}

View file

@ -1,3 +1,9 @@
#[derive(Clone)]
pub struct FormattedPath {
pub path: String,
pub status: alejandra_engine::format::Status,
}
pub fn parse(args: Vec<String>) -> clap::ArgMatches {
clap::Command::new("Alejandra")
.about("The Uncompromising Nix Code Formatter.")
@ -15,6 +21,11 @@ pub fn parse(args: Vec<String>) -> clap::ArgMatches {
.multiple_occurrences(true)
.takes_value(true),
)
.arg(
clap::Arg::new("check")
.help("Check if the input is already formatted.")
.long("--check"),
)
.term_width(80)
.after_help(indoc::indoc!(
// Let's just use the same sorting as on GitHub
@ -24,65 +35,65 @@ pub fn parse(args: Vec<String>) -> clap::ArgMatches {
//
// Feel free to add here your contact/blog/links if you want
"
The program will exit with status code:
1, if any error occurs.
2, if --check was used and any file was changed.
0, otherwise.
Shaped with love by:
- Kevin Amado ~ @kamadorueda on GitHub, matrix.org and Gmail.
- Thomas Bereknyei ~ @tomberek on GitHub and matrix.org.
- David Arnold ~ @blaggacao on GitHub and matrix.org.
- Vincent Ambo ~ @tazjin on GitHub.
- Mr Hedgehog ~ @ModdedGamers on GitHub.
Kevin Amado ~ @kamadorueda on GitHub, matrix.org and Gmail.
Thomas Bereknyei ~ @tomberek on GitHub and matrix.org.
David Arnold ~ @blaggacao on GitHub and matrix.org.
Vincent Ambo ~ @tazjin on GitHub.
Mr Hedgehog ~ @ModdedGamers on GitHub.
"
))
.get_matches_from(args)
}
pub fn stdin() -> std::io::Result<()> {
pub fn stdin() -> FormattedPath {
use std::io::Read;
let mut before = String::new();
let path = "<anonymous file on stdin>".to_string();
eprintln!("Formatting stdin, run with --help to see all options.");
let mut stdin = String::new();
std::io::stdin().read_to_string(&mut stdin).unwrap();
let stdout = alejandra_engine::format::string("stdin".to_string(), stdin)?;
print!("{}", stdout);
std::io::stdin().read_to_string(&mut before).unwrap();
Ok(())
let (status, data) =
alejandra_engine::format::in_memory(path.clone(), before.clone());
print!("{}", data);
FormattedPath { path, status }
}
pub fn simple(paths: Vec<String>) -> std::io::Result<()> {
pub fn simple(paths: Vec<String>) -> Vec<FormattedPath> {
use rayon::prelude::*;
eprintln!("Formatting {} files.", paths.len());
eprintln!("Formatting: {} files", paths.len());
let (results, errors): (Vec<_>, Vec<_>) = paths
paths
.par_iter()
.map(|path| -> std::io::Result<bool> {
alejandra_engine::format::file(path.to_string()).map(|changed| {
.map(|path| {
let status = alejandra_engine::format::in_place(path.clone());
if let alejandra_engine::format::Status::Changed(changed) = status {
if changed {
eprintln!("Formatted: {}", &path);
eprintln!("Changed: {}", &path);
}
changed
})
}
FormattedPath { path: path.clone(), status }
})
.partition(Result::is_ok);
eprintln!(
"Changed: {}",
results.into_iter().map(Result::unwrap).filter(|&x| x).count(),
);
eprintln!("Errors: {}", errors.len(),);
Ok(())
.collect()
}
pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
pub fn tui(paths: Vec<String>) -> std::io::Result<Vec<FormattedPath>> {
use rayon::prelude::*;
use termion::{input::TermRead, raw::IntoRawMode};
struct FormattedPath {
path: String,
result: std::io::Result<bool>,
}
enum Event {
FormattedPath(FormattedPath),
FormattingFinished,
@ -91,11 +102,12 @@ pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
}
let paths_to_format = paths.len();
let mut formatted_paths = std::collections::LinkedList::new();
let stdout = std::io::stderr().into_raw_mode()?;
// let stdout = termion::screen::AlternateScreen::from(stdout);
let backend = tui::backend::TermionBackend::new(stdout);
let mut terminal = tui::Terminal::new(backend)?;
terminal.clear()?;
let (sender, receiver) = std::sync::mpsc::channel();
@ -128,10 +140,10 @@ pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
let sender_finished = sender;
std::thread::spawn(move || {
paths.into_par_iter().for_each_with(sender_paths, |sender, path| {
let result = alejandra_engine::format::file(path.clone());
let status = alejandra_engine::format::in_place(path.clone());
if let Err(error) = sender
.send(Event::FormattedPath(FormattedPath { path, result }))
.send(Event::FormattedPath(FormattedPath { path, status }))
{
eprintln!("{}", error);
}
@ -142,36 +154,32 @@ pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
}
});
terminal.clear()?;
let mut finished = false;
let mut paths_with_errors: usize = 0;
let mut paths_changed: usize = 0;
let mut paths_unchanged: usize = 0;
let mut formatted_paths = std::collections::LinkedList::new();
while !finished {
loop {
if let Ok(event) = receiver.try_recv() {
match event {
Event::FormattedPath(formatted_path) => {
match formatted_path.result {
Ok(changed) => {
if changed {
match &formatted_path.status {
alejandra_engine::format::Status::Changed(
changed,
) => {
if *changed {
paths_changed += 1;
} else {
paths_unchanged += 1;
}
}
Err(_) => {
alejandra_engine::format::Status::Error(_) => {
paths_with_errors += 1;
}
};
formatted_paths.push_back(formatted_path);
if formatted_paths.len() > 8 {
formatted_paths.pop_front();
}
}
Event::FormattingFinished => {
finished = true;
@ -253,23 +261,29 @@ pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
let logger = tui::widgets::Paragraph::new(
formatted_paths
.iter()
.rev()
.take(8)
.map(|formatted_path| {
tui::text::Spans::from(vec![
match &formatted_path.result {
Ok(changed) => tui::text::Span::styled(
if *changed {
"CHANGED "
match formatted_path.status {
alejandra_engine::format::Status::Changed(
changed,
) => tui::text::Span::styled(
if changed {
"CHANGED "
} else {
"UNCHANGED "
"OK "
},
tui::style::Style::default()
.fg(tui::style::Color::Green),
),
Err(_) => tui::text::Span::styled(
"ERROR ",
tui::style::Style::default()
.fg(tui::style::Color::Red),
),
alejandra_engine::format::Status::Error(_) => {
tui::text::Span::styled(
"ERROR ",
tui::style::Style::default()
.fg(tui::style::Color::Red),
)
}
},
tui::text::Span::raw(formatted_path.path.clone()),
])
@ -289,5 +303,5 @@ pub fn tui(paths: Vec<String>) -> std::io::Result<()> {
})?;
}
Ok(())
Ok(formatted_paths.iter().cloned().collect())
}

View file

@ -1,7 +1,9 @@
fn main() -> std::io::Result<()> {
let matches = alejandra_cli::cli::parse(std::env::args().collect());
match matches.values_of("include") {
let check = matches.is_present("check");
let formatted_paths = match matches.values_of("include") {
Some(include) => {
let include = include.collect();
let exclude = match matches.values_of("exclude") {
@ -16,15 +18,65 @@ fn main() -> std::io::Result<()> {
&& atty::is(atty::Stream::Stdin)
&& atty::is(atty::Stream::Stdout)
{
alejandra_cli::cli::tui(paths)?;
alejandra_cli::cli::tui(paths)?
} else {
alejandra_cli::cli::simple(paths)?;
alejandra_cli::cli::simple(paths)
}
}
None => {
alejandra_cli::cli::stdin()?;
None => vec![alejandra_cli::cli::stdin()],
};
let errors = formatted_paths
.iter()
.filter(|formatted_path| {
matches!(
formatted_path.status,
alejandra_engine::format::Status::Error(_)
)
})
.count();
if errors > 0 {
eprintln!();
eprintln!(
"Failed! We encountered {} error{} at:",
errors,
if errors > 0 { "s" } else { "" }
);
for formatted_path in formatted_paths {
if let alejandra_engine::format::Status::Error(error) =
formatted_path.status
{
eprintln!(" {}: {}", formatted_path.path, &error);
}
}
std::process::exit(1);
}
let changed = formatted_paths
.iter()
.filter(|formatted_path| match formatted_path.status {
alejandra_engine::format::Status::Changed(changed) => changed,
_ => false,
})
.count();
if changed > 0 {
eprintln!();
eprintln!(
"Success! {} file{} {} changed",
changed,
if changed > 0 { "s" } else { "" },
if changed > 0 { "were" } else { "was" },
);
if check {
std::process::exit(2);
} else {
std::process::exit(0);
}
}
eprintln!();
eprintln!("Success! Your code complies the Alejandra style");
std::process::exit(0);
}

View file

@ -1,42 +1,56 @@
pub fn string(path: String, string: String) -> std::io::Result<String> {
let tokens = rnix::tokenizer::Tokenizer::new(&string);
#[derive(Clone)]
pub enum Status {
Error(String),
Changed(bool),
}
impl From<std::io::Error> for Status {
fn from(error: std::io::Error) -> Status {
Status::Error(error.to_string())
}
}
pub fn in_memory(path: String, before: String) -> (Status, String) {
let tokens = rnix::tokenizer::Tokenizer::new(&before);
let ast = rnix::parser::parse(tokens);
let errors = ast.errors();
if !errors.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
errors[0].to_string(),
));
return (Status::Error(errors[0].to_string()), before);
}
let green_node =
crate::builder::build(ast.node().into(), false, path, true).unwrap();
let after = crate::builder::build(ast.node().into(), false, path, true)
.unwrap()
.to_string();
Ok(green_node.to_string())
}
pub fn string_or_passthrough(path: String, before: String) -> String {
match crate::format::string(path, before.clone()) {
Ok(after) => after,
Err(_) => before,
if before == after {
(Status::Changed(false), after)
} else {
(Status::Changed(true), after)
}
}
pub fn file(path: String) -> std::io::Result<bool> {
pub fn in_place(path: String) -> Status {
use std::io::Write;
let input = std::fs::read_to_string(&path)?;
let input_clone = input.clone();
let input_bytes = input_clone.as_bytes();
match std::fs::read_to_string(&path) {
Ok(before) => {
let (status, data) = crate::format::in_memory(path.clone(), before);
let output = crate::format::string(path.clone(), input)?;
let output_bytes = output.as_bytes();
if let Status::Changed(changed) = status {
if changed {
return match std::fs::File::create(path) {
Ok(mut file) => match file.write_all(data.as_bytes()) {
Ok(_) => status,
Err(error) => Status::from(error),
},
Err(error) => Status::from(error),
};
}
}
let changed = input_bytes != output_bytes;
if changed {
std::fs::File::create(path)?.write_all(output_bytes)?;
status
}
Err(error) => Status::from(error),
}
Ok(changed)
}

View file

@ -14,10 +14,8 @@ fn cases() {
let path_in = format!("tests/cases/{}/in", case);
let path_out = format!("tests/cases/{}/out", case);
let content_in = std::fs::read_to_string(path_in.clone()).unwrap();
let content_got = alejandra_engine::format::string_or_passthrough(
path_in,
content_in.clone(),
);
let content_got =
alejandra_engine::format::in_memory(path_in, content_in.clone()).1;
if should_update {
std::fs::File::create(&path_out)