fpr_sh/
lib.rs

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    fn stub(&self) -> String {
273        let doc = &self.doc;
274        let name = &self.name;
275        let name_raw = &self.name_raw;
276        let generics = &self.generics;
277        let ret_ty = &self.ret_ty;
278        let fn_args = &self.fn_args;
279        let ret_raw_ty = &self.ret_raw_ty;
280        let generics_where = &self.generics_where;
281        format!(
282            r#"{doc}
283#[allow(dead_code)]
284pub fn {name_raw}{generics}({fn_args}) -> Result<{ret_raw_ty}, String> {generics_where} {{
285    todo!()
286}}
287{doc}
288#[allow(dead_code)]
289pub fn {name}{generics}({fn_args}) -> Result<{ret_ty}, String> {generics_where} {{
290    todo!()
291}}
292"#
293        )
294    }
295}
296
297fn scan_scripts<P: AsRef<Path>>(d: P) -> Res<Vec<String>> {
298    let mut b = Vec::new();
299    for d in fs_read_dir(d)? {
300        let p = d
301            .map_err(|e| format!("Failed to read entry because '{e}'"))?
302            .path();
303        if p.is_dir() {
304            continue;
305        }
306        let pl = p.to_string_lossy().to_string();
307        let f = p
308            .file_name()
309            .ok_or(format!("Expected filename in '{pl}'"))?
310            .to_string_lossy()
311            .to_string();
312        b.push(f);
313    }
314    Ok(b)
315}
316
317pub fn run(src: &str, dst_file: &str) -> Res<()> {
318    let src = PathBuf::from(env_var("CARGO_MANIFEST_DIR")?).join(src);
319    let out = PathBuf::from(env_var("OUT_DIR")?).join(dst_file);
320
321    let p = Pats {
322        start: reg("^# start metadata$")?,
323        end: reg("^# end metadata$")?,
324        ty: reg("^# type ([^ ]+)$")?,
325        arg: reg(r#"^([^=]+)=([()"1-9${}:@]+)(.*)$"#)?,
326        sh_var: reg(r#"([(${"@:]+)([0-9]+)(\}"\))?"#)?,
327    };
328
329    let scripts = scan_scripts(&src)?;
330    gen_code(&p, &src, &out, &scripts)?;
331    Ok(())
332}
333
334fn gen_code(pats: &Pats, src: &PathBuf, out: &PathBuf, scripts: &[String]) -> Res<()> {
335    let mut f = BufWriter::new(
336        File::create(out)
337            .map_err(|e| format!("Failed to create '{}' because '{e}'", out.to_string_lossy()))?,
338    );
339    let mut w = |a: &str| -> Res<()> {
340        Ok(write!(f, "{}", a).map_err(|e| {
341            format!(
342                "Failed to write to '{}' because '{e}'",
343                out.to_string_lossy()
344            )
345        })?)
346    };
347
348    for s in scripts {
349        let p = src.join(s);
350        let script = Script::new(p.as_path(), pats)?;
351        w(&script.code())?;
352    }
353    Ok(())
354}