ctsh_proc/
lib.rs

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}