Skip to content

Commit

Permalink
Integrate shfmt (#128)
Browse files Browse the repository at this point in the history
* add shfmt compatibility

* add formatter tests

* add BashFormatter::as_cmd method

* Update src/modules/formatter.rs

Co-authored-by: Phoenix Himself <pkaras.it@gmail.com>

* refactor formatter.rs a little bit

* fix(rust): some changes

* fix(rust): some changes

* feat(readme): added line

* fix(merge): previous

* Update src/compiler.rs

Co-authored-by: Hubert Jabłoński <hubik080@gmail.com>

* Update src/modules/formatter.rs

Co-authored-by: Hubert Jabłoński <hubik080@gmail.com>

* Update src/modules/formatter.rs

Co-authored-by: Hubert Jabłoński <hubik080@gmail.com>

---------

Co-authored-by: Phoenix Himself <pkaras.it@gmail.com>
Co-authored-by: Daniele Scasciafratte <mte90net@gmail.com>
Co-authored-by: Hubert Jabłoński <hubik080@gmail.com>
  • Loading branch information
4 people authored Jul 23, 2024
1 parent 969d06c commit 03c6be2
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 20 deletions.
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");

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

0 comments on commit 03c6be2

Please sign in to comment.