1#![forbid(unsafe_code)]
2
3use std::{
4 io::Write,
5 path::{Path, PathBuf},
6};
7
8use proc_macro::TokenStream;
9use proc_macro_error::{abort, abort_call_site, proc_macro_error};
10use syn::parse_macro_input;
11
12mod syntax;
13use self::syntax::{Disposition, Script};
14
15#[proc_macro_error]
16#[proc_macro]
17pub fn ctsh(input: TokenStream) -> TokenStream {
18 let Script { commands } = parse_macro_input!(input as Script);
19
20 let mut dir: PathBuf = std::env::var_os("CARGO_MANIFEST_DIR")
21 .expect("Could not determine CARGO_MANIFEST_DIR")
22 .into();
23 dir = dir.canonicalize().expect("CARGO_MANIFEST_DIR is not a valid directory");
24
25 let items = {
26 let mut stmts = false;
27 let mut exprs = false;
28 for command in &commands {
29 if let syntax::Item::Run(syntax::Run {
30 disposition: Some(disposition),
31 ..
32 }) = command
33 {
34 if let syntax::Disposition::Stmts { .. } = disposition {
35 stmts = true;
36 } else {
37 exprs = true;
38 }
39 }
40 }
41
42 if stmts && exprs {
43 abort_call_site!("Cannot mix expressions and items");
44 }
45 stmts
46 };
47
48 let mut tokens = Vec::new();
49 let mut tempdirs = Vec::new();
50 for command in commands {
51 use syntax::Item::*;
52 match command {
53 File(command) => file(&dir, &command),
54 Run(command) => tokens.extend(run(&dir, &command)),
55 Cd(command) => tempdirs.extend(cd(&mut dir, command)),
56 }
57 }
58 drop(tempdirs);
59
60 match tokens.len() {
61 0 => quote::quote!().into(),
62 1 => tokens.pop().unwrap().into(),
63 _ => {
64 if items {
65 quote::quote!(#(#tokens)*).into()
66 } else {
67 quote::quote!((#(#tokens,)*)).into()
68 }
69 }
70 }
71}
72
73fn file(dir: &Path, syntax::File { name, content }: &syntax::File) {
74 let path = dir.join(name);
75 let mut file =
76 std::fs::File::create(path).unwrap_or_else(|err| abort_call_site!("Cannot create file {}: {}", name, err));
77 file
78 .write_all(content)
79 .unwrap_or_else(|err| abort_call_site!("Cannot write to file {}: {}", name, err));
80}
81
82fn run(
83 dir: &Path,
84 syntax::Run {
85 commands,
86 disposition,
87 span,
88 }: &syntax::Run,
89) -> Option<proc_macro2::TokenStream> {
90 let cwd = dir.to_path_buf();
91
92 let mut cmd_iter = commands.iter().rev();
93 let last_command = cmd_iter.next().expect("There should be at least one command");
94 let pipe = last_command.build(&cwd).stdout_capture();
95
96 let pipe = cmd_iter.fold(pipe, |pipe, stage| stage.build(&cwd).pipe(pipe));
97
98 let result = pipe
99 .run()
100 .unwrap_or_else(|err| abort!(span, "Cannot run pipe: {}", err));
101
102 disposition
103 .as_ref()
104 .map(|disposition| output_to_tokens(result.stdout, disposition))
105}
106
107fn output_to_tokens(output: Vec<u8>, disposition: &Disposition) -> proc_macro2::TokenStream {
108 match disposition {
109 Disposition::Bytes { span } => {
110 let tokens = syn::LitByteStr::new(&output, *span);
111 quote::quote!(#tokens)
112 }
113 Disposition::String { span } => {
114 let value = String::from_utf8(output).unwrap_or_else(|_err| abort!(span, "Output of command is not valid UTF-8"));
115 let tokens = syn::LitStr::new(&value, *span);
116 quote::quote!(#tokens)
117 }
118 Disposition::Expr { span } => {
119 let value = String::from_utf8(output).unwrap_or_else(|_err| abort!(span, "Output of command is not valid UTF-8"));
120 let tokens: syn::Expr = syn::parse_str(&value)
121 .unwrap_or_else(|err| abort!(span, "Output of command is not a valid rust expression: {}", err));
122 quote::quote!(#tokens)
123 }
124 Disposition::Stmts { span } => {
125 let value = String::from_utf8(output).unwrap_or_else(|_err| abort!(span, "Output of command is not valid UTF-8"));
126 let syntax::Stmts { items } = syn::parse_str(&value)
127 .unwrap_or_else(|err| abort!(span, "Output of command is not a valid rust expression: {}", err));
128 quote::quote!(#(#items)*)
129 }
130 }
131}
132
133fn cd(dir: &mut PathBuf, syntax::Cd { target, span }: syntax::Cd) -> Option<tempfile::TempDir> {
134 match target {
135 syntax::CdTarget::Path(target) => {
136 dir.push(&target);
137 *dir = dir
138 .canonicalize()
139 .unwrap_or_else(|err| abort!(span, "Could not change directory to {:?}: {}", &target, err));
140 None
141 }
142 syntax::CdTarget::Temp => {
143 let tempdir =
144 tempfile::tempdir().unwrap_or_else(|err| abort!(span, "Could not create temporary directory: {}", err));
145 *dir = tempdir
146 .path()
147 .canonicalize()
148 .unwrap_or_else(|err| abort!(span, "Could not change directory to {:?}: {}", &target, err));
149 Some(tempdir)
150 }
151 }
152}