Basic functionality works.

TODO:
* Allow calling scripts from other directories.
* Don't use OS-dependent path processing.
This commit is contained in:
Sage Vaillancourt 2021-08-22 10:54:59 -04:00
commit a13cd20685
4 changed files with 214 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
Cargo.lock

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "rush"
version = "0.1.0"
authors = ["Sage Vaillancourt <sagev9000@tutanota.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"

21
example.rsh Executable file
View File

@ -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%

181
src/main.rs Normal file
View File

@ -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<Chars>,
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<String> {
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(())
}