1use std::{
2 fs::File,
3 io::{BufRead, BufReader, BufWriter, Write},
4 path::{Path, PathBuf},
5 str::FromStr,
6};
7
8use fpr_cli::*;
9use itertools::Itertools;
10use regex::Regex;
11
12type Res<T> = Result<T, MyErr>;
13
14struct Pats {
15 start: Regex,
16 ty: Regex,
17 arg: Regex,
18 sh_var: Regex,
19 end: Regex,
20}
21
22enum Type {
23 Text,
24 Interactive,
25}
26
27struct Arg {
28 name: String,
29 num: usize,
30 varidic: bool,
31 desc: String,
32}
33#[derive(PartialEq, Eq, PartialOrd, Ord)]
34struct Script {
35 doc: String,
36 name: String,
37 name_raw: String,
38 generics: &'static str,
39 fn_args: String,
40 ret_raw_ty: &'static str,
41 ret_ty: &'static str,
42 generics_where: &'static str,
43 res: &'static str,
44 cmd: &'static str,
45 cmd_args: String,
46 exec: &'static str,
47 ret_raw: String,
48 ret: String,
49 fn_args2: String,
50}
51impl Script {
52 fn new(script_path: &Path, p: &Pats) -> Res<Self> {
53 let fps = script_path.to_string_lossy();
54 let name = script_path
55 .file_stem()
56 .ok_or(format!("Expected a file steam in '{fps}'"))?
57 .to_string_lossy()
58 .to_string();
59
60 let f = BufReader::new(
61 File::open(&script_path)
62 .map_err(|e| format!("Failed to open '{fps}' because '{e}'"))?,
63 );
64
65 let lines = (|| {
66 let mut inner_lines = Vec::<String>::new();
67 let mut b = false;
68
69 for l in f.lines() {
70 let l = l.unwrap();
71 if p.start.find(&l).is_some() {
72 b = true;
73 continue;
74 }
75 if b && p.end.find(&l).is_some() {
76 break;
77 }
78
79 if b {
80 inner_lines.push(l);
81 }
82 }
83
84 if !b {
85 panic!(
86 "Not all tags present for '{fps}': {:?}, {:?}",
87 p.start, p.start
88 )
89 }
90
91 inner_lines
92 })();
93
94 if lines.is_empty() {
95 panic!("Expected type at first line.")
96 };
97
98 let ty =
99 p.ty.captures(&lines[0])
100 .ok_or(format!("Expected one type tag for '{fps}': {:?}", p.ty))?;
101 let ty = match &ty[1] {
102 "text" => Type::Text,
103 "interactive" => Type::Interactive,
104 e => Err(format!("Unexpected type for '{fps}': {e}"))?,
105 };
106
107 let args = lines
108 .iter()
109 .skip(1)
110 .map(|l| -> Result<_, _> { p.arg.captures(&l).ok_or(format!("Malformed line '{l}'")) })
111 .map(|m| -> Result<_, String> {
112 let m = m?;
113 let v = m[2].to_owned();
114 let v_caps = p
115 .sh_var
116 .captures(&v)
117 .ok_or(format!("Malformed variable '{v}'"))?;
118 let (num, varidic) = if v_caps.get(3).is_some()
119 && v_caps[1].to_string() == r#"("${@:"# {
120 (v_caps[2].to_owned(), true)
121 } else if v_caps.get(3).is_none() && v_caps[1].to_string() == r#"$"# {
122 (v_caps[2].to_owned(), false)
123 } else {
124 return Err(format!("Malformed variable '{v}' '{:?}'", v_caps));
125 };
126 let num = usize::from_str(&num)
127 .map_err(|e| format!("Failed to parse '{num}' as a number because '{e}'"))?;
128 Ok(Arg {
129 name: m[1].to_owned(),
130 num,
131 varidic,
132 desc: m[3].to_owned(),
133 })
134 })
135 .collect::<Result<Vec<_>, _>>()
136 .map_err(|e| format!("Failed to parse file '{fps}' because '{e}"))?;
137
138 args.iter().enumerate().for_each(|(i, a)| {
139 if i + 1 != a.num {
140 panic!("Argument not ordered at {i} for '{:?}'", &script_path);
141 }
142 if a.varidic && a.num != args.len() {
143 panic!(
144 "Only the last argument can be varidic in '{:?}' '{}'",
145 &script_path, a.name
146 );
147 }
148 });
149
150 let is_varidic = 0 < args.iter().filter(|a| a.varidic).count();
151
152 let doc = args
153 .iter()
154 .filter(|a| !a.desc.is_empty())
155 .map(|a| format!("/// {}{}", a.name, a.desc))
156 .join("\n");
157 let generics = if is_varidic { "<I, S>" } else { "" };
158 let generics_where = if is_varidic {
159 "where I: IntoIterator<Item = S>, S: std::convert::AsRef<std::ffi::OsStr>"
160 } else {
161 ""
162 };
163 let fn_args = args
164 .iter()
165 .map(|a| {
166 if !a.varidic {
167 format!("{}: &str", a.name)
168 } else {
169 format!("{}: I", a.name)
170 }
171 })
172 .join(", ");
173 let fn_args2 = args.iter().map(|a| format!("{}", a.name)).join(", ");
174 let ret_ty = match ty {
175 Type::Text => "String",
176 Type::Interactive => "()",
177 };
178 let ret_raw_ty = match ty {
179 Type::Text => "Vec<u8>",
180 Type::Interactive => "()",
181 };
182 let res = match ty {
183 Type::Text => "let r = ",
184 Type::Interactive => "let _ = ",
185 };
186 let cmd = "bash";
187 let cmd_args = format!(
188 r#".arg("-c").arg(include_str!("{}")).arg(""){}"#,
189 fps,
190 if args.is_empty() {
191 format!("")
192 } else {
193 args.iter()
194 .map(|a| {
195 if !a.varidic {
196 format!(".arg({})", a.name)
197 } else {
198 format!(".args({})", a.name)
199 }
200 })
201 .join("")
202 }
203 );
204 let exec = match ty {
205 Type::Text => "output",
206 Type::Interactive => "status",
207 };
208 let ret = match ty {
209 Type::Text => format!(
210 r#"Ok(String::from_utf8(r).map_err(|e| format!("Output of '{cmd}' not valid UTF-8. '{{e}}'"))?)"#
211 ),
212 Type::Interactive => format!("Ok(())"),
213 };
214 let ret_raw = match ty {
215 Type::Text => format!(r#"Ok(r.stdout)"#),
216 Type::Interactive => format!("Ok(())"),
217 };
218
219 let name_raw = format!("{name}_raw");
220 Ok(Self {
221 doc,
222 name,
223 name_raw,
224 generics,
225 ret_ty,
226 fn_args,
227 ret_raw_ty,
228 generics_where,
229 res,
230 cmd,
231 cmd_args,
232 exec,
233 ret_raw,
234 ret,
235 fn_args2,
236 })
237 }
238
239 fn code(&self) -> String {
240 let doc = &self.doc;
241 let name = &self.name;
242 let name_raw = &self.name_raw;
243 let generics = &self.generics;
244 let ret_ty = &self.ret_ty;
245 let fn_args = &self.fn_args;
246 let ret_raw_ty = &self.ret_raw_ty;
247 let generics_where = &self.generics_where;
248 let res = &self.res;
249 let cmd = &self.cmd;
250 let cmd_args = &self.cmd_args;
251 let exec = &self.exec;
252 let ret_raw = &self.ret_raw;
253 let ret = &self.ret;
254 let fn_args2 = &self.fn_args2;
255 format!(
256 r#"{doc}
257#[allow(dead_code)]
258pub fn {name_raw}{generics}({fn_args}) -> Result<{ret_raw_ty}, String> {generics_where} {{
259 {res}std::process::Command::new("{cmd}"){cmd_args}.{exec}().map_err(|e| format!("Command '{cmd}' error '{{e}}'"))?;
260 {ret_raw}
261}}
262{doc}
263#[allow(dead_code)]
264pub fn {name}{generics}({fn_args}) -> Result<{ret_ty}, String> {generics_where} {{
265 {res}{name_raw}({fn_args2})?;
266 {ret}
267}}
268"#
269 )
270 }
271}
272
273fn scan_scripts<P: AsRef<Path>>(d: P) -> Res<Vec<String>> {
274 let mut b = Vec::new();
275 for d in fs_read_dir(d)? {
276 let p = d
277 .map_err(|e| format!("Failed to read entry because '{e}'"))?
278 .path();
279 if p.is_dir() {
280 continue;
281 }
282 let pl = p.to_string_lossy().to_string();
283 let f = p
284 .file_name()
285 .ok_or(format!("Expected filename in '{pl}'"))?
286 .to_string_lossy()
287 .to_string();
288 b.push(f);
289 }
290 Ok(b)
291}
292
293pub fn run(src: &str, dst_file: &str) -> Res<()> {
294 let src = PathBuf::from(env_var("CARGO_MANIFEST_DIR")?).join(src);
295 let out = PathBuf::from(env_var("OUT_DIR")?).join(dst_file);
296
297 let p = Pats {
298 start: reg("^# start metadata$")?,
299 end: reg("^# end metadata$")?,
300 ty: reg("^# type ([^ ]+)$")?,
301 arg: reg(r#"^([^=]+)=([()"1-9${}:@]+)(.*)$"#)?,
302 sh_var: reg(r#"([(${"@:]+)([0-9]+)(\}"\))?"#)?,
303 };
304
305 let scripts = scan_scripts(&src)?;
306 gen_code(&p, &src, &out, &scripts)?;
307 Ok(())
308}
309
310fn gen_code(pats: &Pats, src: &PathBuf, out: &PathBuf, scripts: &[String]) -> Res<()> {
311 let mut f = BufWriter::new(
312 File::create(out)
313 .map_err(|e| format!("Failed to create '{}' because '{e}'", out.to_string_lossy()))?,
314 );
315 let mut w = |a: &str| -> Res<()> {
316 Ok(write!(f, "{}", a).map_err(|e| {
317 format!(
318 "Failed to write to '{}' because '{e}'",
319 out.to_string_lossy()
320 )
321 })?)
322 };
323
324 for s in scripts {
325 let p = src.join(s);
326 let script = Script::new(p.as_path(), pats)?;
327 w(&script.code())?;
328 }
329 Ok(())
330}