Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate shfmt #128

Merged
merged 15 commits into from
Jul 23, 2024
20 changes: 15 additions & 5 deletions src/compiler.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use heraclitus_compiler::prelude::*;
use crate::modules::block::Block;
use crate::modules::formatter::BashFormatter;
use crate::translate::check_all_blocks;
use crate::utils::{ParserMetadata, TranslateMetadata};
use crate::translate::module::TranslateModule;
use crate::rules;
use crate::{rules, Cli};
use std::process::Command;
use std::env;
use std::time::Instant;
Expand All @@ -15,14 +16,16 @@ const AMBER_DEBUG_TIME: &str = "AMBER_DEBUG_TIME";

pub struct AmberCompiler {
pub cc: Compiler,
pub path: Option<String>
pub path: Option<String>,
pub cli_opts: Cli
}

impl AmberCompiler {
pub fn new(code: String, path: Option<String>) -> AmberCompiler {
pub fn new(code: String, path: Option<String>, cli_opts: Cli) -> AmberCompiler {
AmberCompiler {
cc: Compiler::new("Amber", rules::get_rules()),
path
path,
cli_opts
}.load_code(code)
}

Expand Down Expand Up @@ -108,7 +111,14 @@ impl AmberCompiler {
println!("[{}]\tin\t{}ms\t{pathname}", "Translate".magenta(), time.elapsed().as_millis());
}
result.push(block.translate(&mut meta));
result.join("\n")
let res = result.join("\n");

if ! self.cli_opts.disable_format {
Mte90 marked this conversation as resolved.
Show resolved Hide resolved
if let Some(formatter) = BashFormatter::get_available() {
return formatter.format(res);
}
}
res
}

pub fn compile(&self) -> Result<(Vec<Message>, String), Message> {
Expand Down
27 changes: 21 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,38 @@ use std::io::prelude::*;
use std::path::PathBuf;
use std::process::Command;

#[derive(Parser)]
#[derive(Parser, Clone, Debug)]
#[command(version, arg_required_else_help(true))]
struct Cli {
pub struct Cli {
input: Option<PathBuf>,
output: Option<PathBuf>,

/// Code to evaluate
#[arg(short, long)]
eval: Option<String>,

/// Don't format the output file
#[arg(long)]
disable_format: bool
}

impl Default for Cli {
fn default() -> Self {
Self {
input: None,
output: None,
eval: None,
disable_format: false
}
}
}

fn main() {
let cli = Cli::parse();

if let Some(code) = cli.eval {
if let Some(code) = cli.eval.clone() {
let code = format!("import * from \"std\"\n{code}");
match AmberCompiler::new(code, None).compile() {
match AmberCompiler::new(code, None, cli).compile() {
Ok((messages, code)) => {
messages.iter().for_each(|m| m.show());
(!messages.is_empty()).then(|| render_dash());
Expand All @@ -43,12 +58,12 @@ fn main() {
std::process::exit(1);
}
}
} else if let Some(input) = cli.input {
} else if let Some(input) = cli.input.clone() {
let input = String::from(input.to_string_lossy());

match fs::read_to_string(&input) {
Ok(code) => {
match AmberCompiler::new(code, Some(input)).compile() {
match AmberCompiler::new(code, Some(input), cli.clone()).compile() {
Ok((messages, code)) => {
messages.iter().for_each(|m| m.show());
// Save to the output file
Expand Down
76 changes: 76 additions & 0 deletions src/modules/formatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::{io::{BufWriter, Write}, process::{Command, Stdio}};


/// This mechanism is built to support multiple formatters.
///
/// The idea is that amber should find the one installed, verify that its compatible and use the best one possible.
#[derive(Debug, Clone, Copy)]
#[allow(non_camel_case_types)]
pub enum BashFormatter {
/// https://github.com/mvdan/sh
shfmt
}

impl BashFormatter {
/// Get all available formatters, ordered by: best ones at the start, worser at the end
Mte90 marked this conversation as resolved.
Show resolved Hide resolved
pub fn get_all() -> Vec<BashFormatter> {
vec![
BashFormatter::shfmt
]
}

/// Get available formatter
pub fn get_available() -> Option<BashFormatter> {
let all = Self::get_all();
all.iter().find(|x| x.is_available()).map(|x| *x)
Mte90 marked this conversation as resolved.
Show resolved Hide resolved
}

/// Check if current formatter is present in $PATH
pub fn is_available(self: &Self) -> bool {
match self {
BashFormatter::shfmt =>
Command::new("shfmt")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|mut x| x.wait())
.is_ok()
}
}

pub fn as_cmd<T: From<&'static str>>(self: &Self) -> T {
match self {
BashFormatter::shfmt => "shfmt".into()
}
}

/// Format code using the formatter
pub fn format(self: &Self, code: String) -> String {
match self {
BashFormatter::shfmt => {
let mut command = Command::new("shfmt")
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.arg("-i").arg("4") // identation
b1ek marked this conversation as resolved.
Show resolved Hide resolved
.arg("-ln").arg("bash") // language
.spawn().expect("Couldn't spawn shfmt");

{
let cmd_stdin = command.stdin.as_mut().expect("Couldn't get shfmt's stdin");
let mut writer = BufWriter::new(cmd_stdin);
writer.write_all(code.as_bytes()).expect("Couldn't write code to shfmt");
writer.flush().expect("Couldn't flush shfmt's stdin");
}

let res = command.wait_with_output().expect("Couldn't wait for shfmt");

// let mut stdout = command.stdout.expect("Couldn't get shfmt's stdout");
// let mut out = String::new();
// stdout.read_to_string(&mut out).expect("Couldn't read shfmt's output");
b1ek marked this conversation as resolved.
Show resolved Hide resolved

String::from_utf8(res.stdout).expect("shfmt returned non utf-8 output")
}
}
}
}
3 changes: 2 additions & 1 deletion src/modules/imports/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::modules::variable::variable_name_extensions;
use crate::utils::context::{Context, FunctionDecl};
use crate::utils::{ParserMetadata, TranslateMetadata};
use crate::translate::module::TranslateModule;
use crate::Cli;
use super::import_string::ImportString;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -87,7 +88,7 @@ impl Import {
}

fn handle_compile_code(&mut self, meta: &mut ParserMetadata, imported_code: String) -> SyntaxResult {
match AmberCompiler::new(imported_code.clone(), Some(self.path.value.clone())).tokenize() {
match AmberCompiler::new(imported_code.clone(), Some(self.path.value.clone()), Cli::default()).tokenize() {
Ok(tokens) => {
let mut block = Block::new();
// Save snapshot of current file
Expand Down
1 change: 1 addition & 0 deletions src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod types;
pub mod imports;
pub mod main;
pub mod builtin;
pub mod formatter;

#[macro_export]
macro_rules! handle_types {
Expand Down
33 changes: 33 additions & 0 deletions src/tests/formatter.rs
Ph0enixKM marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::{env, fs::{self, Permissions}, os::unix::fs::PermissionsExt};

use crate::modules::formatter::BashFormatter;

fn create_fake_binary(fmt: BashFormatter) {
let body = if cfg!(unix) {
"#!/usr/bin/env bash\nexit 0"
} else {
panic!("this test is not available for non-unix platforms")
};

let name: String = fmt.as_cmd();

fs::write(&name, body).expect("Couldn't write fake script");
fs::set_permissions(&name, Permissions::from_mode(0o755)).expect("Couldn't set perms for fake script");
}

#[test]
fn all_exist() {
let path = env::var("PATH").expect("Cannot get $PATH");

env::set_var("PATH", format!("{path}:./")); // temporary unset to ensure that shfmt exists in $PATH
let fmts = BashFormatter::get_all();
for fmt in fmts {
create_fake_binary(fmt);
assert_eq!(fmt.is_available(), true);
assert_eq!(BashFormatter::get_available().is_some(), true);
fs::remove_file(fmt.as_cmd::<String>()).expect("Couldn't remove formatter's fake binary");
}

env::set_var("PATH", &path);
assert_eq!(env::var("PATH").expect("Cannot get $PATH"), path);
}
Mte90 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod formatter;
pub mod validity;
3 changes: 2 additions & 1 deletion src/tests/validity.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::compiler::AmberCompiler;
use crate::Cli;

macro_rules! test_amber {
($code:expr, $result:expr) => {
{
match AmberCompiler::new($code.to_string(), None).test_eval() {
match AmberCompiler::new($code.to_string(), None, Cli::default()).test_eval() {
Ok(result) => assert_eq!(result.trim(), $result),
Err(err) => panic!("ERROR: {}", err.message.unwrap())
}
Expand Down