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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

# Amber

Programming language that compiles to Bash. It's a high level programming language that makes it easy to create shell scripts. It's particularly well suited for cloud services.
Programming language that compiles to Bash. It's a high level programming language that makes it easy to create shell scripts. It's particularly well suited for cloud services.
If [shfmt](https://github.com/mvdan/sh) it is present in the machine it will be used after the compilation to prettify the Bash code generated.

> [!Warning]
> This software is not ready for extended usage.
Expand Down
17 changes: 14 additions & 3 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use chrono::prelude::*;
use crate::docs::module::DocumentationModule;
use itertools::Itertools;
use crate::modules::block::Block;
use crate::rules;
use crate::modules::formatter::BashFormatter;
use crate::{rules, Cli};
use crate::translate::check_all_blocks;
use crate::translate::module::TranslateModule;
use crate::utils::{ParserMetadata, TranslateMetadata};
Expand All @@ -24,13 +25,15 @@ const AMBER_DEBUG_TIME: &str = "AMBER_DEBUG_TIME";
pub struct AmberCompiler {
pub cc: Compiler,
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,
cli_opts
}
.load_code(AmberCompiler::strip_off_shebang(code))
}
Expand Down Expand Up @@ -150,7 +153,15 @@ impl AmberCompiler {
time.elapsed().as_millis()
);
}
let res = result.join("\n");

let mut res = result.join("\n");

if !self.cli_opts.disable_format {
if let Some(formatter) = BashFormatter::get_available() {
res = formatter.format(res);
}
}

let header = [
include_str!("header.sh"),
&("# version: ".to_owned() + option_env!("CARGO_PKG_VERSION").unwrap().to_string().as_str()),
Expand Down
56 changes: 43 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ 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>,

Expand All @@ -31,27 +31,57 @@ struct Cli {

/// Generate docs
#[arg(long)]
docs: bool
docs: bool,

/// 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,
docs: false,
disable_format: false
}
}
}

fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
if cli.docs {
handle_docs(cli)?;
} else if let Some(code) = cli.eval {
handle_eval(code)?;
} else if let Some(ref code) = cli.eval {
handle_eval(code.to_string(), cli)?;
} else {
handle_compile(cli)?;
}
Ok(())
}

fn handle_compile(cli: Cli) -> Result<(), Box<dyn Error>> {
if let Some(input) = cli.input {
fn handle_compile(cli: Cli) -> Result<(), Box<dyn Error>> {
if let Some(code) = cli.eval.clone() {
let code = format!("import * from \"std\"\n{code}");
match AmberCompiler::new(code, None, cli).compile() {
Ok((messages, code)) => {
messages.iter().for_each(|m| m.show());
(!messages.is_empty()).then(|| render_dash());
let exit_status = AmberCompiler::execute(code, &vec![])?;
std::process::exit(exit_status.code().unwrap_or(1));
}
Err(err) => {
err.show();
std::process::exit(1);
}
}
} 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 Expand Up @@ -92,9 +122,9 @@ fn handle_compile(cli: Cli) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn handle_eval(code: String) -> Result<(), Box<dyn Error>> {
fn handle_eval(code: String, cli: Cli) -> Result<(), Box<dyn Error>> {
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 @@ -109,15 +139,15 @@ fn handle_eval(code: String) -> Result<(), Box<dyn Error>> {
}

fn handle_docs(cli: Cli) -> Result<(), Box<dyn Error>> {
if let Some(input) = cli.input {
if let Some(ref input) = cli.input {
let input = String::from(input.to_string_lossy());
let output = {
let out = cli.output.unwrap_or_else(|| PathBuf::from("docs"));
let out = cli.output.clone().unwrap_or_else(|| PathBuf::from("docs"));
String::from(out.to_string_lossy())
};
match fs::read_to_string(&input) {
Ok(code) => {
match AmberCompiler::new(code, Some(input)).generate_docs(output) {
match AmberCompiler::new(code, Some(input), cli).generate_docs(output) {
Ok(_) => Ok(()),
Err(err) => {
err.show();
Expand Down
75 changes: 75 additions & 0 deletions src/modules/formatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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: best ones at the start, worst at the end
pub fn get_all() -> Vec<BashFormatter> {
vec![
BashFormatter::shfmt
]
}

/// Get available formatter
pub fn get_available() -> Option<BashFormatter> {
Self::get_all()
.iter()
.find(|fmt| fmt.is_available())
.map(|fmt| *fmt)
}

/// 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()
}
}

#[allow(dead_code)] // used in tests
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") // indentation
.arg("-ln").arg("bash") // language
.spawn().expect("Couldn't spawn shfmt");
Mte90 marked this conversation as resolved.
Show resolved Hide resolved

{
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");

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 @@ -9,6 +9,7 @@ use crate::stdlib;
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 @@ -97,7 +98,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
6 changes: 4 additions & 2 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
use crate::compiler::AmberCompiler;
use crate::Cli;

pub mod cli;
pub mod formatter;
pub mod stdlib;
pub mod validity;

#[macro_export]
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_end_matches('\n'), $result),
Err(err) => panic!("ERROR: {}", err.message.unwrap()),
}
}};
}

pub fn compile_code<T: Into<String>>(code: T) -> String {
AmberCompiler::new(code.into(), None).compile().unwrap().1
AmberCompiler::new(code.into(), None, Cli::default()).compile().unwrap().1
}
1 change: 1 addition & 0 deletions src/tests/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use test_generator::test_resources;
use crate::compiler::AmberCompiler;
use crate::test_amber;
use crate::tests::compile_code;
use crate::Cli;
use std::path::Path;
use std::fs;
use std::time::Duration;
Expand Down
1 change: 1 addition & 0 deletions src/tests/validity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extern crate test_generator;
use test_generator::test_resources;
use crate::compiler::AmberCompiler;
use crate::test_amber;
use crate::Cli;
use std::fs;
use std::path::Path;

Expand Down
Loading