use std::{
env::{consts, var},
fs::{self, File},
io::{BufRead, BufReader, BufWriter, Write},
path::{Path, PathBuf},
str::FromStr,
};
use fpr_cli::*;
use itertools::Itertools;
use regex::Regex;
type Res<T> = Result<T, MyErr>;
const KNOWN_PLATFORMS: &[&str] = &["all", "mac", "win"];
const DEFAULT_PLATFORM: &str = "all";
struct Pats {
start: Regex,
ty: Regex,
arg: Regex,
sh_var: Regex,
end: Regex,
}
enum Type {
Text,
Interactive,
}
struct Arg {
name: String,
num: usize,
varidic: bool,
desc: String,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Script {
doc: String,
name: String,
name_raw: String,
generics: &'static str,
fn_args: String,
ret_raw_ty: &'static str,
ret_ty: &'static str,
generics_where: &'static str,
res: &'static str,
cmd: &'static str,
cmd_args: String,
exec: &'static str,
ret_raw: String,
ret: String,
fn_args2: String,
}
impl Script {
fn new(script_path: &Path, dest_path: &Path, p: &Pats) -> Res<Self> {
let fps = script_path.to_string_lossy();
let name = script_path
.file_stem()
.ok_or(format!("Expected a file steam in '{fps}'"))?
.to_string_lossy()
.to_string();
let f = BufReader::new(
File::open(&script_path)
.map_err(|e| format!("Failed to open '{fps}' because '{e}'"))?,
);
let lines = (|| {
let mut inner_lines = Vec::<String>::new();
let mut b = false;
for l in f.lines() {
let l = l.unwrap();
if p.start.find(&l).is_some() {
b = true;
continue;
}
if b && p.end.find(&l).is_some() {
break;
}
if b {
inner_lines.push(l);
}
}
if !b {
panic!(
"Not all tags present for '{fps}': {:?}, {:?}",
p.start, p.start
)
}
inner_lines
})();
if lines.is_empty() {
panic!("Expected type at first line.")
};
let ty =
p.ty.captures(&lines[0])
.ok_or(format!("Expected one type tag for '{fps}': {:?}", p.ty))?;
let ty = match &ty[1] {
"text" => Type::Text,
"interactive" => Type::Interactive,
e => Err(format!("Unexpected type for '{fps}': {e}"))?,
};
let args = lines
.iter()
.skip(1)
.map(|l| -> Result<_, _> { p.arg.captures(&l).ok_or(format!("Malformed line '{l}'")) })
.map(|m| -> Result<_, String> {
let m = m?;
let v = m[2].to_owned();
let v_caps = p
.sh_var
.captures(&v)
.ok_or(format!("Malformed variable '{v}'"))?;
let (num, varidic) = if v_caps.get(3).is_some()
&& v_caps[1].to_string() == r#"("${@:"# {
(v_caps[2].to_owned(), true)
} else if v_caps.get(3).is_none() && v_caps[1].to_string() == r#"$"# {
(v_caps[2].to_owned(), false)
} else {
return Err(format!("Malformed variable '{v}' '{:?}'", v_caps));
};
let num = usize::from_str(&num)
.map_err(|e| format!("Failed to parse '{num}' as a number because '{e}'"))?;
Ok(Arg {
name: m[1].to_owned(),
num,
varidic,
desc: m[3].to_owned(),
})
})
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Failed to parse file '{fps}' because '{e}"))?;
args.iter().enumerate().for_each(|(i, a)| {
if i + 1 != a.num {
panic!("Argument not ordered at {i} for '{:?}'", &script_path);
}
if a.varidic && a.num != args.len() {
panic!(
"Only the last argument can be varidic in '{:?}' '{}'",
&script_path, a.name
);
}
});
let is_varidic = 0 < args.iter().filter(|a| a.varidic).count();
let doc = args
.iter()
.filter(|a| !a.desc.is_empty())
.map(|a| format!("/// {}{}", a.name, a.desc))
.join("\n");
let generics = if is_varidic { "<I, S>" } else { "" };
let generics_where = if is_varidic {
"where I: IntoIterator<Item = S>, S: std::convert::AsRef<std::ffi::OsStr>"
} else {
""
};
let fn_args = args
.iter()
.map(|a| {
if !a.varidic {
format!("{}: &str", a.name)
} else {
format!("{}: I", a.name)
}
})
.join(", ");
let fn_args2 = args.iter().map(|a| format!("{}", a.name)).join(", ");
let ret_ty = match ty {
Type::Text => "String",
Type::Interactive => "()",
};
let ret_raw_ty = match ty {
Type::Text => "Vec<u8>",
Type::Interactive => "()",
};
let res = match ty {
Type::Text => "let r = ",
Type::Interactive => "let _ = ",
};
let cmd = "bash";
let cmd_args = format!(
r#".arg("-c").arg(include_str!("{}")).arg(""){}"#,
fps,
if args.is_empty() {
format!("")
} else {
args.iter()
.map(|a| {
if !a.varidic {
format!(".arg({})", a.name)
} else {
format!(".args({})", a.name)
}
})
.join("")
}
);
let exec = match ty {
Type::Text => "output",
Type::Interactive => "status",
};
let ret = match ty {
Type::Text => format!(
r#"Ok(String::from_utf8(r).map_err(|e| format!("Output of '{cmd}' not valid UTF-8. '{{e}}'"))?)"#
),
Type::Interactive => format!("Ok(())"),
};
let ret_raw = match ty {
Type::Text => format!(r#"Ok(r.stdout)"#),
Type::Interactive => format!("Ok(())"),
};
let name_raw = format!("{name}_raw");
Ok(Self {
doc,
name,
name_raw,
generics,
ret_ty,
fn_args,
ret_raw_ty,
generics_where,
res,
cmd,
cmd_args,
exec,
ret_raw,
ret,
fn_args2,
})
}
fn code(&self) -> String {
let doc = &self.doc;
let name = &self.name;
let name_raw = &self.name_raw;
let generics = &self.generics;
let ret_ty = &self.ret_ty;
let fn_args = &self.fn_args;
let ret_raw_ty = &self.ret_raw_ty;
let generics_where = &self.generics_where;
let res = &self.res;
let cmd = &self.cmd;
let cmd_args = &self.cmd_args;
let exec = &self.exec;
let ret_raw = &self.ret_raw;
let ret = &self.ret;
let fn_args2 = &self.fn_args2;
format!(
r#"{doc}
#[allow(dead_code)]
pub fn {name_raw}{generics}({fn_args}) -> Result<{ret_raw_ty}, String> {generics_where} {{
{res}std::process::Command::new("{cmd}"){cmd_args}.{exec}().map_err(|e| format!("Command '{cmd}' error '{{e}}'"))?;
{ret_raw}
}}
{doc}
#[allow(dead_code)]
pub fn {name}{generics}({fn_args}) -> Result<{ret_ty}, String> {generics_where} {{
{res}{name_raw}({fn_args2})?;
{ret}
}}
"#
)
}
fn stub(&self) -> String {
let doc = &self.doc;
let name = &self.name;
let name_raw = &self.name_raw;
let generics = &self.generics;
let ret_ty = &self.ret_ty;
let fn_args = &self.fn_args;
let ret_raw_ty = &self.ret_raw_ty;
let generics_where = &self.generics_where;
format!(
r#"{doc}
#[allow(dead_code)]
pub fn {name_raw}{generics}({fn_args}) -> Result<{ret_raw_ty}, String> {generics_where} {{
todo!()
}}
{doc}
#[allow(dead_code)]
pub fn {name}{generics}({fn_args}) -> Result<{ret_ty}, String> {generics_where} {{
todo!()
}}
"#
)
}
}
fn scan_scripts<P: AsRef<Path>>(d: P) -> Res<Vec<String>> {
let mut b = Vec::new();
for d in fs_read_dir(d)? {
let p = d
.map_err(|e| format!("Failed to read entry because '{e}'"))?
.path();
if p.is_file() {
continue;
}
let pl = p.to_string_lossy().to_string();
let f = p
.file_name()
.ok_or(format!("Expected filename in '{pl}'"))?
.to_string_lossy()
.to_string();
if !KNOWN_PLATFORMS.iter().any(|e| (*e).eq(&f)) {
println!("Skipping unknown platform '{f}'");
continue;
}
for d in fs_read_dir(p)? {
let p = d
.map_err(|e| format!("Failed to read entry because '{e}'"))?
.path();
if p.is_dir() {
continue;
}
let pl = p.to_string_lossy().to_string();
let f = p
.file_name()
.ok_or(format!("Expected filename in '{pl}'"))?
.to_string_lossy()
.to_string();
b.push(f);
}
}
b.sort();
b.dedup();
Ok(b)
}
pub fn run(src: &str, dst_file: &str) -> Res<()> {
let src = PathBuf::from(env_var("CARGO_MANIFEST_DIR")?).join(src);
let out = PathBuf::from(env_var("OUT_DIR")?).join(dst_file);
let p = Pats {
start: reg("^# start metadata$")?,
end: reg("^# end metadata$")?,
ty: reg("^# type ([^ ]+)$")?,
arg: reg(r#"^([^=]+)=([()"1-9${}:@]+)(.*)$"#)?,
sh_var: reg(r#"([(${"@:]+)([0-9]+)(\}"\))?"#)?,
};
let scripts = scan_scripts(&src)?;
gen_code(&p, &src, &out, &scripts)?;
Ok(())
}
fn gen_code(pats: &Pats, src: &PathBuf, out: &PathBuf, scripts: &[String]) -> Res<()> {
let mut f = BufWriter::new(
File::create(out)
.map_err(|e| format!("Failed to create '{}' because '{e}'", out.to_string_lossy()))?,
);
let mut w = |a: &str| -> Res<()> {
Ok(write!(f, "{}", a).map_err(|e| {
format!(
"Failed to write to '{}' because '{e}'",
out.to_string_lossy()
)
})?)
};
let plat = match consts::OS {
"linux" => "win",
"macos" => "mac",
e => {
println!("Unknown platform '{e},' defaulting to '{DEFAULT_PLATFORM}'");
"all"
}
};
for s in scripts {
let mut b = Vec::new();
for p in KNOWN_PLATFORMS {
let p = src.join(p).join(s);
if p.exists() {
b.push(Script::new(p.as_path(), out.as_path(), pats)?);
}
}
b.sort();
b.dedup();
if b.len() != 1 {
Err(format!("Conflicting implementations for script '{s}'"))?
}
let p = src.join(plat).join(s);
let script = &b[0];
if p.exists() {
w(&script.code())?;
continue;
}
let p = src.join(DEFAULT_PLATFORM).join(s);
if p.exists() {
w(&script.code())?;
continue;
}
w(&script.stub())?;
}
Ok(())
}