1 use crate::{ast, update};
2 
3 use std::{fmt::Write, path::Path};
4 
emit(xflags: &ast::XFlags) -> String5 pub(crate) fn emit(xflags: &ast::XFlags) -> String {
6     let mut buf = String::new();
7 
8     emit_cmd(&mut buf, &xflags.cmd);
9     blank_line(&mut buf);
10     emit_api(&mut buf, xflags);
11 
12     if std::env::var("UPDATE_XFLAGS").is_ok() {
13         if let Some(src) = &xflags.src {
14             update::in_place(&buf, Path::new(src.as_str()))
15         } else {
16             update::stdout(&buf);
17         }
18     }
19 
20     if xflags.src.is_some() {
21         buf.clear()
22     }
23 
24     blank_line(&mut buf);
25     emit_impls(&mut buf, &xflags);
26     emit_help(&mut buf, xflags);
27 
28     buf
29 }
30 
31 macro_rules! w {
32     ($($tt:tt)*) => {
33         drop(write!($($tt)*))
34     };
35 }
36 
emit_cmd(buf: &mut String, cmd: &ast::Cmd)37 fn emit_cmd(buf: &mut String, cmd: &ast::Cmd) {
38     w!(buf, "#[derive(Debug)]\n");
39     w!(buf, "pub struct {}", cmd.ident());
40     if cmd.args.is_empty() && cmd.flags.is_empty() && cmd.subcommands.is_empty() {
41         w!(buf, ";\n");
42         return;
43     }
44     w!(buf, " {{\n");
45 
46     for arg in &cmd.args {
47         let ty = gen_arg_ty(arg.arity, &arg.val.ty);
48         w!(buf, "    pub {}: {},\n", arg.val.ident(), ty);
49     }
50 
51     if !cmd.args.is_empty() && !cmd.flags.is_empty() {
52         blank_line(buf);
53     }
54 
55     for flag in &cmd.flags {
56         let ty = gen_flag_ty(flag.arity, flag.val.as_ref().map(|it| &it.ty));
57         w!(buf, "    pub {}: {},\n", flag.ident(), ty);
58     }
59 
60     if cmd.has_subcommands() {
61         w!(buf, "    pub subcommand: {},\n", cmd.cmd_enum_ident());
62     }
63     w!(buf, "}}\n");
64 
65     if cmd.has_subcommands() {
66         blank_line(buf);
67         w!(buf, "#[derive(Debug)]\n");
68         w!(buf, "pub enum {} {{\n", cmd.cmd_enum_ident());
69         for sub in &cmd.subcommands {
70             let name = camel(&sub.name);
71             w!(buf, "    {}({}),\n", name, name);
72         }
73         w!(buf, "}}\n");
74 
75         for sub in &cmd.subcommands {
76             blank_line(buf);
77             emit_cmd(buf, sub);
78         }
79     }
80 }
81 
gen_flag_ty(arity: ast::Arity, ty: Option<&ast::Ty>) -> String82 fn gen_flag_ty(arity: ast::Arity, ty: Option<&ast::Ty>) -> String {
83     match ty {
84         None => match arity {
85             ast::Arity::Optional => "bool".to_string(),
86             ast::Arity::Required => "()".to_string(),
87             ast::Arity::Repeated => "u32".to_string(),
88         },
89         Some(ty) => gen_arg_ty(arity, ty),
90     }
91 }
92 
gen_arg_ty(arity: ast::Arity, ty: &ast::Ty) -> String93 fn gen_arg_ty(arity: ast::Arity, ty: &ast::Ty) -> String {
94     let ty = match ty {
95         ast::Ty::PathBuf => "PathBuf".into(),
96         ast::Ty::OsString => "OsString".into(),
97         ast::Ty::FromStr(it) => it.clone(),
98     };
99     match arity {
100         ast::Arity::Optional => format!("Option<{}>", ty),
101         ast::Arity::Required => ty,
102         ast::Arity::Repeated => format!("Vec<{}>", ty),
103     }
104 }
105 
emit_api(buf: &mut String, xflags: &ast::XFlags)106 fn emit_api(buf: &mut String, xflags: &ast::XFlags) {
107     w!(buf, "impl {} {{\n", camel(&xflags.cmd.name));
108 
109     w!(buf, "    pub const HELP: &'static str = Self::HELP_;\n");
110     blank_line(buf);
111 
112     w!(buf, "    #[allow(dead_code)]\n");
113     w!(buf, "    pub fn from_env() -> xflags::Result<Self> {{\n");
114     w!(buf, "        Self::from_env_()\n");
115     w!(buf, "    }}\n");
116     blank_line(buf);
117 
118     w!(buf, "    #[allow(dead_code)]\n");
119     w!(buf, "    pub fn from_vec(args: Vec<std::ffi::OsString>) -> xflags::Result<Self> {{\n");
120     w!(buf, "        Self::from_vec_(args)\n");
121     w!(buf, "    }}\n");
122     w!(buf, "}}\n");
123 }
124 
emit_impls(buf: &mut String, xflags: &ast::XFlags) -> ()125 fn emit_impls(buf: &mut String, xflags: &ast::XFlags) -> () {
126     w!(buf, "impl {} {{\n", camel(&xflags.cmd.name));
127     w!(buf, "    fn from_env_() -> xflags::Result<Self> {{\n");
128     w!(buf, "        let mut p = xflags::rt::Parser::new_from_env();\n");
129     w!(buf, "        Self::parse_(&mut p)\n");
130     w!(buf, "    }}\n");
131     w!(buf, "    fn from_vec_(args: Vec<std::ffi::OsString>) -> xflags::Result<Self> {{\n");
132     w!(buf, "        let mut p = xflags::rt::Parser::new(args);\n");
133     w!(buf, "        Self::parse_(&mut p)\n");
134     w!(buf, "    }}\n");
135     w!(buf, "}}\n");
136     blank_line(buf);
137     emit_impls_rec(buf, &xflags.cmd)
138 }
139 
emit_impls_rec(buf: &mut String, cmd: &ast::Cmd) -> ()140 fn emit_impls_rec(buf: &mut String, cmd: &ast::Cmd) -> () {
141     emit_impl(buf, cmd);
142     for sub in &cmd.subcommands {
143         blank_line(buf);
144         emit_impls_rec(buf, sub);
145     }
146 }
147 
emit_impl(buf: &mut String, cmd: &ast::Cmd) -> ()148 fn emit_impl(buf: &mut String, cmd: &ast::Cmd) -> () {
149     w!(buf, "impl {} {{\n", camel(&cmd.name));
150     w!(buf, "fn parse_(p_: &mut xflags::rt::Parser) -> xflags::Result<Self> {{\n");
151 
152     for flag in &cmd.flags {
153         w!(buf, "let mut {} = Vec::new();\n", flag.ident());
154     }
155     blank_line(buf);
156 
157     if !cmd.args.is_empty() {
158         for arg in &cmd.args {
159             w!(buf, "let mut {} = (false, Vec::new());\n", arg.val.ident());
160         }
161         blank_line(buf);
162     }
163 
164     if cmd.has_subcommands() {
165         w!(buf, "let mut sub_ = None;");
166         blank_line(buf);
167     }
168 
169     w!(buf, "while let Some(arg_) = p_.pop_flag() {{\n");
170     w!(buf, "match arg_ {{\n");
171     {
172         w!(buf, "Ok(flag_) => match flag_.as_str() {{\n");
173         for flag in &cmd.flags {
174             w!(buf, "\"--{}\"", flag.name);
175             if let Some(short) = &flag.short {
176                 w!(buf, "| \"-{}\"", short);
177             }
178             w!(buf, " => {}.push(", flag.ident());
179             match &flag.val {
180                 Some(val) => match &val.ty {
181                     ast::Ty::OsString | ast::Ty::PathBuf => {
182                         w!(buf, "p_.next_value(&flag_)?.into()")
183                     }
184                     ast::Ty::FromStr(ty) => {
185                         w!(buf, "p_.next_value_from_str::<{}>(&flag_)?", ty)
186                     }
187                 },
188                 None => w!(buf, "()"),
189             }
190             w!(buf, "),");
191         }
192         if cmd.default_subcommand().is_some() {
193             w!(buf, "_ => {{ p_.push_back(Ok(flag_)); break; }}");
194         } else {
195             w!(buf, "_ => return Err(p_.unexpected_flag(&flag_)),");
196         }
197         w!(buf, "}}\n");
198     }
199     {
200         w!(buf, "Err(arg_) => {{\n");
201         if cmd.has_subcommands() {
202             w!(buf, "match arg_.to_str().unwrap_or(\"\") {{\n");
203             for sub in cmd.named_subcommands() {
204                 w!(buf, "\"{}\" => {{\n", sub.name);
205                 w!(
206                     buf,
207                     "sub_ = Some({}::{}({}::parse_(p_)?));",
208                     cmd.cmd_enum_ident(),
209                     sub.ident(),
210                     sub.ident()
211                 );
212                 w!(buf, "break;");
213                 w!(buf, "}}\n");
214             }
215             w!(buf, "_ => (),\n");
216             w!(buf, "}}\n");
217         }
218 
219         for arg in &cmd.args {
220             let done = match arg.arity {
221                 ast::Arity::Optional | ast::Arity::Required => "done_ @ ",
222                 ast::Arity::Repeated => "",
223             };
224             w!(buf, "if let ({}false, buf_) = &mut {} {{\n", done, arg.val.ident());
225             w!(buf, "buf_.push(");
226             match &arg.val.ty {
227                 ast::Ty::OsString | ast::Ty::PathBuf => {
228                     w!(buf, "arg_.into()")
229                 }
230                 ast::Ty::FromStr(ty) => {
231                     w!(buf, "p_.value_from_str::<{}>(\"{}\", arg_)?", ty, arg.val.name);
232                 }
233             }
234             w!(buf, ");\n");
235             match arg.arity {
236                 ast::Arity::Optional | ast::Arity::Required => {
237                     w!(buf, "*done_ = true;\n");
238                 }
239                 ast::Arity::Repeated => (),
240             }
241             w!(buf, "continue;\n");
242             w!(buf, "}}\n");
243         }
244         if cmd.default_subcommand().is_some() {
245             w!(buf, "p_.push_back(Err(arg_)); break;");
246         } else {
247             w!(buf, "return Err(p_.unexpected_arg(arg_));");
248         }
249 
250         w!(buf, "}}\n");
251     }
252     w!(buf, "}}\n");
253     w!(buf, "}}\n");
254 
255     if let Some(sub) = cmd.default_subcommand() {
256         w!(buf, "if sub_.is_none() {{\n");
257         w!(
258             buf,
259             "sub_ = Some({}::{}({}::parse_(p_)?));",
260             cmd.cmd_enum_ident(),
261             sub.ident(),
262             sub.ident()
263         );
264         w!(buf, "}}\n");
265     }
266 
267     w!(buf, "Ok(Self {{\n");
268     if !cmd.args.is_empty() {
269         for arg in &cmd.args {
270             let val = &arg.val;
271             w!(buf, "{}: ", val.ident());
272             match arg.arity {
273                 ast::Arity::Optional => {
274                     w!(buf, "p_.optional(\"{}\", {}.1)?", val.name, val.ident())
275                 }
276                 ast::Arity::Required => {
277                     w!(buf, "p_.required(\"{}\", {}.1)?", val.name, val.ident())
278                 }
279                 ast::Arity::Repeated => w!(buf, "{}.1", val.ident()),
280             }
281             w!(buf, ",\n");
282         }
283         blank_line(buf);
284     }
285 
286     for flag in &cmd.flags {
287         w!(buf, "{}: ", flag.ident());
288         match &flag.val {
289             Some(_val) => match flag.arity {
290                 ast::Arity::Optional => {
291                     w!(buf, "p_.optional(\"--{}\", {})?", flag.name, flag.ident())
292                 }
293                 ast::Arity::Required => {
294                     w!(buf, "p_.required(\"--{}\", {})?", flag.name, flag.ident())
295                 }
296                 ast::Arity::Repeated => w!(buf, "{}", flag.ident()),
297             },
298             None => match flag.arity {
299                 ast::Arity::Optional => {
300                     w!(buf, "p_.optional(\"--{}\", {})?.is_some()", flag.name, flag.ident())
301                 }
302                 ast::Arity::Required => {
303                     w!(buf, "p_.required(\"--{}\", {})?", flag.name, flag.ident())
304                 }
305                 ast::Arity::Repeated => w!(buf, "{}.len() as u32", flag.ident()),
306             },
307         }
308         w!(buf, ",\n");
309     }
310     if cmd.has_subcommands() {
311         w!(buf, "subcommand: p_.subcommand(sub_)?,\n");
312     }
313     w!(buf, "}})\n");
314 
315     w!(buf, "}}\n");
316     w!(buf, "}}\n");
317 }
318 
emit_help(buf: &mut String, xflags: &ast::XFlags)319 fn emit_help(buf: &mut String, xflags: &ast::XFlags) {
320     w!(buf, "impl {} {{\n", xflags.cmd.ident());
321 
322     let help = {
323         let mut buf = String::new();
324         help_rec(&mut buf, "", &xflags.cmd);
325         buf
326     };
327     let help = format!("{:?}", help);
328     let help = help.replace("\\n", "\n").replacen("\"", "\"\\\n", 1);
329 
330     w!(buf, "const HELP_: &'static str = {};", help);
331     w!(buf, "}}\n");
332 }
333 
write_lines_indented(buf: &mut String, multiline_str: &str, indent: usize)334 fn write_lines_indented(buf: &mut String, multiline_str: &str, indent: usize) {
335     for line in multiline_str.split('\n').map(str::trim_end) {
336         if line.is_empty() {
337             w!(buf, "\n")
338         } else {
339             w!(buf, "{blank:indent$}{}\n", line, indent = indent, blank = "");
340         }
341     }
342 }
343 
help_rec(buf: &mut String, prefix: &str, cmd: &ast::Cmd)344 fn help_rec(buf: &mut String, prefix: &str, cmd: &ast::Cmd) {
345     w!(buf, "{}{}\n", prefix, cmd.name);
346     if let Some(doc) = &cmd.doc {
347         write_lines_indented(buf, doc, 2);
348     }
349     let indent = if prefix.is_empty() { "" } else { "  " };
350 
351     let args = cmd.args_with_default();
352     if !args.is_empty() {
353         blank_line(buf);
354         w!(buf, "{}ARGS:\n", indent);
355 
356         let mut blank = "";
357         for arg in &args {
358             w!(buf, "{}", blank);
359             blank = "\n";
360 
361             let (l, r) = match arg.arity {
362                 ast::Arity::Optional => ("[", "]"),
363                 ast::Arity::Required => ("<", ">"),
364                 ast::Arity::Repeated => ("<", ">..."),
365             };
366             w!(buf, "    {}{}{}\n", l, arg.val.name, r);
367             if let Some(doc) = &arg.doc {
368                 write_lines_indented(buf, doc, 6)
369             }
370         }
371     }
372 
373     let flags = cmd.flags_with_default();
374     if !flags.is_empty() {
375         blank_line(buf);
376         w!(buf, "{}OPTIONS:\n", indent);
377 
378         let mut blank = "";
379         for flag in &flags {
380             w!(buf, "{}", blank);
381             blank = "\n";
382 
383             let short = flag.short.as_ref().map(|it| format!("-{}, ", it)).unwrap_or_default();
384             let value = flag.val.as_ref().map(|it| format!(" <{}>", it.name)).unwrap_or_default();
385             w!(buf, "    {}--{}{}\n", short, flag.name, value);
386             if let Some(doc) = &flag.doc {
387                 write_lines_indented(buf, doc, 6);
388             }
389         }
390     }
391 
392     let subcommands = cmd.named_subcommands();
393     if !subcommands.is_empty() {
394         if prefix.is_empty() {
395             blank_line(buf);
396             w!(buf, "SUBCOMMANDS:");
397         }
398 
399         let prefix = format!("{}{} ", prefix, cmd.name);
400         for sub in subcommands {
401             blank_line(buf);
402             blank_line(buf);
403             help_rec(buf, &prefix, sub);
404         }
405     }
406 }
407 
408 impl ast::Cmd {
ident(&self) -> String409     fn ident(&self) -> String {
410         camel(&self.name)
411     }
cmd_enum_ident(&self) -> String412     fn cmd_enum_ident(&self) -> String {
413         format!("{}Cmd", self.ident())
414     }
has_subcommands(&self) -> bool415     fn has_subcommands(&self) -> bool {
416         !self.subcommands.is_empty()
417     }
named_subcommands(&self) -> &[ast::Cmd]418     fn named_subcommands(&self) -> &[ast::Cmd] {
419         let start = if self.default { 1 } else { 0 };
420         &self.subcommands[start..]
421     }
default_subcommand(&self) -> Option<&ast::Cmd>422     fn default_subcommand(&self) -> Option<&ast::Cmd> {
423         if self.default {
424             self.subcommands.first()
425         } else {
426             None
427         }
428     }
args_with_default(&self) -> Vec<&ast::Arg>429     fn args_with_default(&self) -> Vec<&ast::Arg> {
430         let mut res = self.args.iter().collect::<Vec<_>>();
431         if let Some(sub) = self.default_subcommand() {
432             res.extend(sub.args_with_default());
433         }
434         res
435     }
flags_with_default(&self) -> Vec<&ast::Flag>436     fn flags_with_default(&self) -> Vec<&ast::Flag> {
437         let mut res = self.flags.iter().collect::<Vec<_>>();
438         if let Some(sub) = self.default_subcommand() {
439             res.extend(sub.flags_with_default())
440         }
441         res
442     }
443 }
444 
445 impl ast::Flag {
ident(&self) -> String446     fn ident(&self) -> String {
447         snake(&self.name)
448     }
449 }
450 
451 impl ast::Val {
ident(&self) -> String452     fn ident(&self) -> String {
453         snake(&self.name)
454     }
455 }
456 
blank_line(buf: &mut String)457 fn blank_line(buf: &mut String) {
458     w!(buf, "\n");
459 }
460 
camel(s: &str) -> String461 fn camel(s: &str) -> String {
462     s.split('-').map(first_upper).collect()
463 }
464 
first_upper(s: &str) -> String465 fn first_upper(s: &str) -> String {
466     s.chars()
467         .next()
468         .map(|it| it.to_ascii_uppercase())
469         .into_iter()
470         .chain(s.chars().skip(1))
471         .collect()
472 }
473 
snake(s: &str) -> String474 fn snake(s: &str) -> String {
475     s.replace('-', "_")
476 }
477 
478 #[cfg(test)]
479 mod tests {
480     use std::{
481         fs,
482         io::Write,
483         path::Path,
484         process::{Command, Stdio},
485     };
486 
reformat(text: String) -> Option<String>487     fn reformat(text: String) -> Option<String> {
488         let mut rustfmt =
489             Command::new("rustfmt").stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap();
490         let mut stdin = rustfmt.stdin.take().unwrap();
491         stdin.write_all(text.as_bytes()).unwrap();
492         drop(stdin);
493         let out = rustfmt.wait_with_output().unwrap();
494         let res = String::from_utf8(out.stdout).unwrap();
495         if res.is_empty() {
496             None
497         } else {
498             Some(res)
499         }
500     }
501 
update_on_disk_if_different(file: &Path, new_contents: String) -> bool502     fn update_on_disk_if_different(file: &Path, new_contents: String) -> bool {
503         let old_contents = fs::read_to_string(file).unwrap_or_default();
504         if old_contents.trim() == new_contents.trim() {
505             return false;
506         }
507         fs::write(file, new_contents).unwrap();
508         true
509     }
510 
511     #[test]
gen_it()512     fn gen_it() {
513         let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/it");
514 
515         let mut did_update = false;
516         for entry in fs::read_dir(test_dir.join("src")).unwrap() {
517             let entry = entry.unwrap();
518 
519             let text = fs::read_to_string(entry.path()).unwrap();
520             let mut lines = text.lines().collect::<Vec<_>>();
521             lines.pop();
522             lines.remove(0);
523             let text = lines.join("\n");
524 
525             let res = crate::compile(&text);
526             let fmt = reformat(res.clone());
527 
528             let code = format!(
529                 "#![allow(unused)]\nuse std::{{ffi::OsString, path::PathBuf}};\n\n{}",
530                 fmt.as_deref().unwrap_or(&res)
531             );
532 
533             let name = entry.file_name();
534             did_update |= update_on_disk_if_different(&test_dir.join(name), code);
535 
536             if fmt.is_none() {
537                 panic!("syntax error");
538             }
539         }
540         if did_update {
541             panic!("generated output changed")
542         }
543     }
544 }
545