xtask_cmdwrap_macro/
lib.rs

1use darling::{ast::NestedMeta, FromMeta};
2use itertools::Itertools;
3use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{
6    parse_macro_input, parse_quote, ExprMethodCall, Field, FnArg, Ident, ImplItemFn, ItemImpl,
7    ItemStruct,
8};
9
10/// Meta items for `#[cmd(...)]`
11#[derive(Debug, FromMeta)]
12struct CmdMeta {
13    #[darling(default)]
14    name: Option<String>,
15}
16
17/// The default prefix of a command argument.
18///
19/// i.e. the "--" in `ls --version`
20fn default_prefix() -> String {
21    "--".to_string()
22}
23
24/// Meta items for #[cmd::opt(...)]
25#[derive(Debug, FromMeta)]
26struct CmdOptMeta {
27    #[darling(default)]
28    no_val: bool,
29
30    #[darling(default)]
31    no_opt: bool,
32
33    #[darling(default)]
34    name: Option<String>,
35
36    #[darling(default=default_prefix)]
37    prefix: String,
38}
39
40/// Generate the constructor for a [std::process::Command] wrapper with a fixed `program` parameter.
41fn constructor(ident: &Ident, program: String) -> ImplItemFn {
42    parse_quote! {
43        pub fn new() -> #ident {
44            #ident(std::process::Command::new(#program))
45        }
46    }
47}
48
49/// Convert the field of a command struct to it's setterI
50fn setter(field: &Field) -> ImplItemFn {
51    // Destruct generic field info
52    let ident = field.ident.as_ref().unwrap();
53    let ty = &(field.ty);
54    let doc_attr = field.attrs.iter().find(|a| a.path().is_ident("doc"));
55    let arg_attr = field.attrs.iter().find(|a| a.path().is_ident("arg"));
56
57    // Parse meta items of #[arg(...)]
58    let cmd_opt;
59    let mut errors = proc_macro2::TokenStream::new();
60    if let Some(a) = arg_attr {
61        cmd_opt = CmdOptMeta::from_meta(&a.meta)
62            .map_err(|e| errors = e.write_errors())
63            .ok();
64    } else {
65        cmd_opt = None;
66        errors = quote! {compile_error!(concat!("Missing #[arg(...)] attribute on field: ", stringify!(#ident)))};
67    }
68    // FIXME: Check that no_val and no_opt aren't both set
69
70    // Generate dummy function on error
71    if !errors.is_empty() {
72        let dummy_ident = format_ident!("{}_err", ident);
73        return parse_quote! {
74            pub fn #dummy_ident() {
75                #errors
76            }
77        };
78    }
79
80    // No error: generate code from options
81    let cmd_opt = cmd_opt.unwrap();
82    let opt_prefix = cmd_opt.prefix;
83    let opt_name: String = match cmd_opt.name {
84        Some(s) => s,
85        None => ident.to_string().to_lowercase(),
86    };
87    let opt = format!("{}{}", opt_prefix, opt_name);
88    let param_val: Option<FnArg> = match cmd_opt.no_val {
89        true => None,
90        false => Some(parse_quote! {val: #ty}),
91    };
92    let arg_val: Option<ExprMethodCall> = match cmd_opt.no_val {
93        true => None,
94        false => Some(parse_quote! {self.0.arg(val)}),
95    };
96    let arg_opt: Option<ExprMethodCall> = match cmd_opt.no_opt {
97        true => None,
98        false => Some(parse_quote! {self.0.arg(#opt)}),
99    };
100
101    // Generate setter function
102    parse_quote! {
103        #doc_attr
104        pub fn #ident(&mut self, #param_val) -> &mut Self {
105            #arg_opt;
106            #arg_val;
107            self
108        }
109    }
110}
111
112/// Get string representation of command
113fn string_repr() -> ImplItemFn {
114    parse_quote! {
115        pub fn string_repr(&self) -> String {
116            let program = self.0.get_program().to_string_lossy();
117            let args = self.0
118                .get_args()
119                .map(|a| a.to_string_lossy())
120                .fold(String::new(), |s, a| s + " " + &a);
121            format!("{}{}", program, args)
122        }
123    }
124}
125
126fn get_inner() -> ImplItemFn {
127    parse_quote! {
128        pub fn cmd(self) -> std::process::Command {
129            self.0
130        }
131    }
132}
133
134/// Implementation of [Into<std::process::Command>]
135fn into_std_command(ident: &Ident) -> ItemImpl {
136    parse_quote! {
137        impl Into<std::process::Command> for #ident {
138            fn into(self) -> std::process::Command {
139                self.cmd()
140            }
141        }
142    }
143}
144
145#[proc_macro_attribute]
146pub fn cmd(attr: TokenStream, input: TokenStream) -> TokenStream {
147    // Parse top level attributes
148    let attr = match NestedMeta::parse_meta_list(attr.into()) {
149        Ok(v) => v,
150        Err(e) => return TokenStream::from(e.into_compile_error()),
151    };
152    let attr = match CmdMeta::from_list(&attr) {
153        Ok(v) => v,
154        Err(e) => return TokenStream::from(e.write_errors()),
155    };
156
157    // Parse the input as Struct
158    let item = parse_macro_input!(input as ItemStruct);
159
160    // Extract useful info
161    let ident = &(item.ident);
162    let name = attr.name.unwrap_or(ident.to_string().to_lowercase());
163    let vis = &(item.vis);
164
165    // Create constructor
166    let constructor = constructor(ident, name);
167
168    // Extract setters
169    let setters = item.fields.iter().map(|f| setter(f)).collect_vec();
170
171    // Create string representation function
172    let string_repr = string_repr();
173
174    // Create getter for inner command item
175    let inner = get_inner();
176
177    // Generate Into<std::process::Command> trait
178    let into_std_command = into_std_command(ident);
179
180    // Generate expanded code
181    let expanded = quote! {
182        #vis struct #ident(std::process::Command);
183        impl #ident {
184            #constructor
185            #inner
186            #(#setters)*
187            #string_repr
188        }
189        #into_std_command
190    };
191
192    TokenStream::from(expanded)
193}