diff --git a/README.md b/README.md index b0792675..244b1ae1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/compiler.rs b/src/compiler.rs index 46382702..1adf9346 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -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}; @@ -24,13 +25,15 @@ const AMBER_DEBUG_TIME: &str = "AMBER_DEBUG_TIME"; pub struct AmberCompiler { pub cc: Compiler, pub path: Option, + pub cli_opts: Cli } impl AmberCompiler { - pub fn new(code: String, path: Option) -> AmberCompiler { + pub fn new(code: String, path: Option, cli_opts: Cli) -> AmberCompiler { AmberCompiler { cc: Compiler::new("Amber", rules::get_rules()), path, + cli_opts } .load_code(AmberCompiler::strip_off_shebang(code)) } @@ -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()), diff --git a/src/main.rs b/src/main.rs index 7e389be0..041ee5c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, output: Option, @@ -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> { 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> { - if let Some(input) = cli.input { +fn handle_compile(cli: Cli) -> Result<(), Box> { + 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 @@ -92,9 +122,9 @@ fn handle_compile(cli: Cli) -> Result<(), Box> { Ok(()) } -fn handle_eval(code: String) -> Result<(), Box> { +fn handle_eval(code: String, cli: Cli) -> Result<(), Box> { 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()); @@ -109,15 +139,15 @@ fn handle_eval(code: String) -> Result<(), Box> { } fn handle_docs(cli: Cli) -> Result<(), Box> { - 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(); diff --git a/src/modules/formatter.rs b/src/modules/formatter.rs new file mode 100644 index 00000000..d8d33ea2 --- /dev/null +++ b/src/modules/formatter.rs @@ -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 { + vec![ + BashFormatter::shfmt + ] + } + + /// Get available formatter + pub fn get_available() -> Option { + 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>(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") + } + } + } +} \ No newline at end of file diff --git a/src/modules/imports/import.rs b/src/modules/imports/import.rs index c62de114..be97d819 100644 --- a/src/modules/imports/import.rs +++ b/src/modules/imports/import.rs @@ -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)] @@ -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 diff --git a/src/modules/mod.rs b/src/modules/mod.rs index d956df42..35d7ffb2 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -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 { diff --git a/src/tests/formatter.rs b/src/tests/formatter.rs new file mode 100644 index 00000000..a7c4f50e --- /dev/null +++ b/src/tests/formatter.rs @@ -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::()).expect("Couldn't remove formatter's fake binary"); + } + + env::set_var("PATH", &path); + assert_eq!(env::var("PATH").expect("Cannot get $PATH"), path); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 70694626..b475f87e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,13 +1,15 @@ 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()), } @@ -15,5 +17,5 @@ macro_rules! test_amber { } pub fn compile_code>(code: T) -> String { - AmberCompiler::new(code.into(), None).compile().unwrap().1 + AmberCompiler::new(code.into(), None, Cli::default()).compile().unwrap().1 } diff --git a/src/tests/stdlib.rs b/src/tests/stdlib.rs index 27478d52..acea5ff9 100644 --- a/src/tests/stdlib.rs +++ b/src/tests/stdlib.rs @@ -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; diff --git a/src/tests/validity.rs b/src/tests/validity.rs index 1466ca7b..c26c5b4c 100644 --- a/src/tests/validity.rs +++ b/src/tests/validity.rs @@ -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;