macros/
lib.rs

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        // if encased in braces, the arg becomes { .. }.to_string()
32        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        // encountered comma with no preceding arg
38        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            // comma gets consumed with take_while
48            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    // consume a comma
66    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}