use regex::Regex;
use std::borrow::Cow;
use std::env;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::process::{self, Command};
use tempfile::Builder;
const TMP_PROJECT_NAME: &str = "evalrs_temp";
struct Args {
snippet: Option<String>,
print_result: bool,
quiet: bool,
release: bool,
}
impl Args {
fn parse() -> noargs::Result<Option<Self>> {
let mut args = noargs::raw_args();
args.metadata_mut().app_name = env!("CARGO_PKG_NAME");
args.metadata_mut().app_description = "Rust code snippet evaluator";
if noargs::VERSION_FLAG.take(&mut args).is_present() {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
return Ok(None);
}
noargs::HELP_FLAG.take_help(&mut args);
let print_result = noargs::flag("print-result")
.short('p')
.doc("Prints the evaluation result using `println!(\"{:?}\", result)`")
.take(&mut args)
.is_present();
let quiet = noargs::flag("quiet")
.short('q')
.doc("Don't show cargo's build messages if succeeded")
.take(&mut args)
.is_present();
let release = noargs::flag("release")
.doc("Builds artifacts in release mode, with optimizations")
.take(&mut args)
.is_present();
let snippet = noargs::arg("[SNIPPET]")
.doc(concat!(
"Rust code snippet to be evaluated. ",
"If this is omitted, the snippet will be read from the standard input."
))
.take(&mut args)
.present()
.map(|a| a.value().to_owned());
if let Some(help) = args.finish()? {
print!("{help}");
return Ok(None);
}
Ok(Some(Self {
snippet,
print_result,
quiet,
release,
}))
}
}
fn main() -> noargs::Result<()> {
let Some(args) = Args::parse()? else {
return Ok(());
};
let input = if let Some(snippet) = args.snippet.clone() {
snippet
} else {
let mut buf = String::new();
io::stdin()
.read_to_string(&mut buf)
.expect("Cannot read string from standard input stream");
buf
};
let manifest = make_manifest(&input);
let source_code = make_source_code(&input, &args);
let project_dir = Builder::new()
.prefix(TMP_PROJECT_NAME)
.tempdir()
.expect("Cannot create temporary directory");
let cache_dir = env::temp_dir().join("evalrs_cache/");
{
let manifest_file = project_dir.path().join("Cargo.toml");
let mut manifest_file =
File::create(manifest_file).expect("Cannot create 'Cargo.toml' file");
manifest_file
.write_all(manifest.as_bytes())
.expect("Cannot write to 'Cargo.toml' file");
}
{
let src_dir = project_dir.path().join("src/");
fs::create_dir(src_dir.clone()).expect("Cannot create 'src/' directory");
let main_file = src_dir.join("main.rs");
let mut main_file = File::create(main_file).expect("Cannot create 'main.rs' file");
main_file
.write_all(source_code.as_bytes())
.expect("Cannot write to 'main.rs' file");
}
{
let target_dir = project_dir.path().join("target/");
let cache_target_dir = cache_dir.join("target/");
fs::create_dir_all(cache_target_dir.clone())
.expect("Cannot create cache 'target/' directory");
fs::rename(cache_target_dir, target_dir)
.expect("Cannot move 'target/' from cache directory");
}
let mut command = Command::new("cargo");
command.arg("build");
if args.quiet {
command.arg("--quiet");
}
if args.release {
command.arg("--release");
}
let mut exit_status = command
.current_dir(project_dir.path())
.spawn()
.expect("Cannot execute 'cargo build'")
.wait()
.expect("Cannot wait cargo process");
if exit_status.success() {
let path = project_dir
.path()
.join("target")
.join(if args.release { "release" } else { "debug" })
.join(TMP_PROJECT_NAME);
exit_status = Command::new(path)
.spawn()
.expect("Cannot execute the built command")
.wait()
.expect("Cannot wait built process");
}
{
let target_dir = project_dir.path().join("target/");
let cache_target_dir = cache_dir.join("target/");
if !cache_target_dir.exists() {
fs::rename(target_dir, cache_target_dir)
.expect("Cannot move 'target/' to cache directory");
}
}
exit_on_fail(exit_status);
Ok(())
}
fn exit_on_fail(exs: process::ExitStatus) {
if !exs.success() {
match exs.code() {
Some(code) => process::exit(code),
None => {
eprintln!("Failed to get exit status code");
process::exit(1);
}
}
}
}
fn make_manifest(input: &str) -> String {
let re = Regex::new(r"extern\s+crate\s+([a-z0-9_]+)\s*;(\s*//(.+))?").unwrap();
let dependencies = re
.captures_iter(input)
.map(|cap| {
if let Some(value) = cap.get(3) {
if value.as_str().contains('=') {
format!("{}\n", value.as_str())
} else {
format!("{} = {}\n", &cap[1], value.as_str())
}
} else {
format!("{} = \"*\"\n", &cap[1])
}
})
.collect::<String>();
format!(
r#"
[package]
name = "{}"
version = "0.0.0"
[dependencies]
{}
"#,
TMP_PROJECT_NAME, dependencies
)
}
fn make_source_code(input: &str, args: &Args) -> String {
let re = Regex::new(r"(?m)^# ").unwrap();
let input = re.replace_all(input, "");
if Regex::new(r"(?m)^\s*fn +main *\( *\)")
.unwrap()
.is_match(&input)
{
return input.to_string();
}
let re = Regex::new(r"(extern\s+crate\s+[a-z0-9_]+\s*;)").unwrap();
let crate_lines = re
.captures_iter(&input)
.map(|cap| format!("{}\n", &cap[1]))
.collect::<String>();
let mut body = re.replace_all(&input, "");
if args.print_result {
body = Cow::from(format!(r#"println!("{{:?}}", {{ {} }});"#, body));
}
format!(
"
{}
fn main() {{
{}
}}",
crate_lines, body
)
}