From a13cd206856e070b5ec27acc5a5d62055eef25b4 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Sun, 22 Aug 2021 10:54:59 -0400 Subject: [PATCH] Basic functionality works. TODO: * Allow calling scripts from other directories. * Don't use OS-dependent path processing. --- .gitignore | 2 + Cargo.toml | 10 +++ example.rsh | 21 ++++++ src/main.rs | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100755 example.rsh create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..127567b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rush" +version = "0.1.0" +authors = ["Sage Vaillancourt "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" diff --git a/example.rsh b/example.rsh new file mode 100755 index 0000000..5561b3a --- /dev/null +++ b/example.rsh @@ -0,0 +1,21 @@ +#!/bin/rush + +let x = %cat example.rsh%; + +if x.contains("example") { + %echo It\\'s a script!% +} + +if %echo some words%.contains("me wo") { + println!("But it's also Rust!"); +} + +let mem = %free -h%.lines().nth(1).unwrap() + .split_whitespace().nth(3).unwrap().to_owned(); + +// for i in 0..10000 { +// println!("x contains %% example? [{}]", x.contains("example")); +// } + +println!("There are {} of memory free", &mem); +%echo hello% diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b0095ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,181 @@ +#![feature(option_result_contains)] + +use anyhow::{bail, Result}; + +use std::env; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::time::{Instant, SystemTime}; + +use std::iter; +use std::str::Chars; + +use std::str; +use std::process::Command; + +#[derive(PartialEq, Clone)] +enum PrintType { + Out, Err, All +} + +fn com(print: &PrintType, command_string: &str) -> String { + let command_output = Command::new("bash").arg("-c") + .arg(command_string) + .output().unwrap(); + + let to_string = |bytes| str::from_utf8(bytes).unwrap().to_string(); + let out = to_string(&command_output.stdout); + let err = to_string(&command_output.stderr); + + match print { + PrintType::Out => { print!("{}", out); out }, + PrintType::Err => { print!("{}", err); err }, + PrintType::All => { + print!("{}", out); + if err.trim().is_empty() { + print!("{}", err); + } + out + }, + } +} + +fn earlier_than(earlier: SystemTime, later: SystemTime) -> bool { + earlier.duration_since(later).is_ok() +} + +fn needs_compilation(script_name: &str) -> bool { + if cfg!(debug_assertions) { + return true; + } + let script_attr = fs::metadata(&script_name); + let compiled_attr = fs::metadata(&output_filename(script_name)); + if let (Ok(sa), Ok(ca)) = (script_attr, compiled_attr) { + let sm = sa.modified(); + let cm = ca.modified(); + if let (Ok(script_mod_time), Ok(compiled_mod_time)) = (sm, cm) { + if earlier_than(compiled_mod_time, script_mod_time) { + return false; + } + } + } + true +} +fn output_filename(script_name: &str) -> String { + format!(".{}.compiled", script_name.replace("./", "")) +} + +fn run_compiled(script_name: &str) { + com(&PrintType::All, &output_filename(script_name)); +} + +fn parse_bash_command( + iter: &mut iter::Peekable, + code: &mut String, + is_expr: bool +) -> Result<()> { + code.push_str(&format!("com({}, \"", !is_expr)); + loop { + if let Some(c) = iter.next() { + if c == '%' { + break; + } + code.push(c); + } else { + bail!("Unexpected end-of-file while parsing bash command"); + } + } + code.push_str("\")"); + let peek = iter.peek(); + if peek.is_none() || peek.contains(&&' ') || peek.contains(&&'\n') { + code.push(';'); + } + Ok(()) +} + +fn collect_code(script_name: &str) -> Result { + let script = fs::read_to_string(&script_name)?; + + let mut code = " +use std::str; +use std::process::Command; + +fn com(print: bool, comcom: &str) -> String { +let s = str::from_utf8(&Command::new(\"bash\").arg(\"-c\") + .arg(comcom).output().unwrap().stdout).unwrap().to_string(); +if print { + print!(\"{}\", s) +}; +return s; +} + +fn main() { +".to_string(); + + let mut is_expr = false; + + let mut iter = script.chars().peekable(); + if script.starts_with("#!/") { + while let Some(c) = iter.next() { + if c == '\n' { + break; + } + } + } + + while let Some(c) = iter.next() { + if c == '%' { + // Double percents count as a single literal percent + if iter.peek().contains(&&'%') { + code.push('%'); + iter.next(); + } else { + parse_bash_command(&mut iter, &mut code, is_expr)?; + } + } else { + code.push(c); + if (c == 'i' && iter.peek().contains(&&'f')) + || (c == '=' && !iter.peek().contains(&&'=')) { + is_expr = true; + } else if (c == '{' || c == ';') && is_expr { + is_expr = false; + } else if c == '\n' && iter.peek().is_some() { + code.push_str(" "); + } + } + } + code.push('}'); + return Ok(code); +} + +fn main() -> Result<()> { + let now = Instant::now(); + if let Some(script_name) = env::args().nth(1) { + if !needs_compilation(&script_name) { + run_compiled(&script_name); + return Ok(()); + } + let code = collect_code(&script_name)?; + let code_filename = format!("/tmp/{}-rush.rs", + script_name.replace("./", "").replace(".", "-")); + let mut file = File::create(&code_filename)?; + file.write_all(code.as_bytes())?; + + if cfg!(debug_assertions) { + println!("[Start Rust]"); + println!("{}", code); + println!("[End Rust]"); + } + + let compile_command = format!( + "rustc {} -o {}", + code_filename, + output_filename(&script_name) + ); + com(&PrintType::Err, &compile_command); + println!("[Compiled in {}ms]\n", now.elapsed().as_millis()); + run_compiled(&script_name); + } + Ok(()) +}