1use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
2use std::str::FromStr;
3
4#[proc_macro]
5pub fn cargs(stream: TokenStream) -> TokenStream {
6 if stream.is_empty() {
7 TokenStream::from_str("{ let __args: [String; 0] = []; __args }").expect("valid Rust")
8 } else {
9 let mut buf = Vec::new();
10
11 let mut stream = stream.into_iter();
12 let stream = stream.by_ref();
13
14 while let Some(arg) = take_arg(stream) {
15 buf.extend(arg);
16 buf.push(Punct::new(',', proc_macro::Spacing::Alone).into());
17 }
18
19 TokenTree::from(Group::new(
20 proc_macro::Delimiter::Bracket,
21 TokenStream::from_iter(buf),
22 ))
23 .into()
24 }
25}
26
27fn take_arg(stream: &mut dyn Iterator<Item = TokenTree>) -> Option<Vec<TokenTree>> {
28 let f = stream.next()?;
29
30 match f {
31 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
33 let x = maybe_wrap_in_quotes(suffix_to_string(vec![g.into()]));
34 expect_comma(stream.next());
35 vec![x].into()
36 }
37 TokenTree::Punct(p) if p.as_char() == ',' => {
39 panic!("expected an argument, but found a comma")
40 }
41 TokenTree::Literal(l) => {
42 let x = maybe_wrap_in_quotes(suffix_to_string(vec![l.into()]));
43 expect_comma(stream.next());
44 vec![x].into()
45 }
46 x => {
47 let s = std::iter::once(x).chain(
49 stream.take_while(|t| !matches!(t, TokenTree::Punct(p) if p.as_char() == ',')),
50 );
51 let mut s = TokenStream::from_iter(s).to_string();
52 s.retain(|c| c != ' ');
53 let s = TokenTree::Literal(Literal::string(&s));
54 suffix_to_string(vec![s]).into()
55 }
56 }
57}
58
59fn suffix_to_string(mut ts: Vec<TokenTree>) -> Vec<TokenTree> {
60 ts.extend(TokenStream::from_str(".to_string()").expect("valid Rust"));
61 ts
62}
63
64fn expect_comma(tt: Option<TokenTree>) {
65 if let Some(n) = tt {
67 if !matches!(
68 n,
69 TokenTree::Punct(p) if p.as_char() == ',')
70 {
71 panic!("expecting a comma delimiter");
72 }
73 }
74}
75
76fn maybe_wrap_in_quotes(expr: Vec<TokenTree>) -> TokenTree {
77 let mut buf = TokenStream::from_str("let x = ").expect("valid Rust");
78 buf.extend(expr);
79 buf.extend([semi_colon()]);
80
81 buf.extend(TokenStream::from_str("if x.contains(' ')").expect("valid Rust"));
82 buf.extend([
83 TokenTree::Group(Group::new(
84 Delimiter::Brace,
85 TokenStream::from_str(r#""\"".to_string() + &x + "\"""#).expect("valid Rust"),
86 )),
87 ident("else"),
88 TokenTree::Group(Group::new(
89 Delimiter::Brace,
90 TokenStream::from_iter([ident("x")]),
91 )),
92 ]);
93
94 TokenTree::Group(Group::new(Delimiter::Brace, buf))
95}
96
97#[proc_macro]
98pub fn cmd(stream: TokenStream) -> TokenStream {
99 let mut stream = stream.into_iter();
100 let stream = stream.by_ref();
101
102 let program = stream.take_while(|t| !matches!(t, TokenTree::Punct(p) if p.as_char() == ':'));
103 let program = TokenStream::from_iter(program).to_string().replace(' ', "");
104 let program = TokenTree::Literal(Literal::string(&program));
105
106 let args = cargs(TokenStream::from_iter(stream));
107
108 let mut stream =
109 TokenStream::from_str("let mut __cmd = ::std::process::Command::new").expect("valid Rust");
110
111 stream.extend([
112 TokenTree::Group(Group::new(Delimiter::Parenthesis, program.into())),
113 semi_colon(),
114 ]);
115 stream.extend(TokenStream::from_str("__cmd.args"));
116 stream.extend([
117 TokenTree::Group(Group::new(Delimiter::Parenthesis, args)),
118 semi_colon(),
119 ident("__cmd"),
120 ]);
121
122 TokenTree::Group(Group::new(Delimiter::Brace, stream)).into()
123}
124
125fn semi_colon() -> TokenTree {
126 TokenTree::Punct(Punct::new(';', Spacing::Alone))
127}
128
129fn ident(s: &str) -> TokenTree {
130 TokenTree::Ident(Ident::new(s, Span::call_site()))
131}