sh_macro/
lib.rs

1use proc_macro2::{Group, Literal, TokenStream, TokenTree};
2use quote::{quote, ToTokens, TokenStreamExt};
3
4enum Arg {
5    Literal(String),
6    Expr(TokenStream),
7}
8
9impl ToTokens for Arg {
10    fn to_tokens(&self, tokens: &mut TokenStream) {
11        match self {
12            Arg::Literal(s) => tokens.append(Literal::string(s)),
13            Arg::Expr(e) => tokens.append_all(e.into_token_stream()),
14        }
15    }
16}
17
18enum ParseState {
19    Cmd,
20    Args,
21    SetSink,
22    DoneSetSink,
23    SetSource,
24    DoneSetSource,
25}
26
27enum Sink {
28    File(String),
29    Expr(TokenStream),
30}
31
32enum Source {
33    File(String),
34    Expr(TokenStream),
35}
36
37struct ShParser {
38    state: ParseState,
39    cmd: Option<String>,
40    args: Vec<Arg>,
41    sink: Option<Sink>,
42    source: Option<Source>,
43}
44
45struct Cmd {
46    cmd: String,
47    args: Vec<Arg>,
48    sink: Option<Sink>,
49    source: Option<Source>,
50}
51
52#[derive(Debug)]
53enum ShTokenTree {
54    Value(String),
55    EndOfLine,
56    Expr(Group),
57    Sink,
58    Source,
59}
60
61impl From<TokenTree> for ShTokenTree {
62    fn from(value: TokenTree) -> Self {
63        match value {
64            TokenTree::Group(g) => ShTokenTree::Expr(g),
65            TokenTree::Ident(value) => ShTokenTree::Value(value.to_string()),
66            TokenTree::Punct(c) if c.as_char() == ';' => ShTokenTree::EndOfLine,
67            TokenTree::Punct(c) if c.as_char() == '>' => ShTokenTree::Sink,
68            TokenTree::Punct(c) if c.as_char() == '<' => ShTokenTree::Source,
69            TokenTree::Punct(c) => panic!("Unexpected token: {c}"),
70            TokenTree::Literal(value) => {
71                let literal = litrs::Literal::from(value);
72                let value = match literal {
73                    litrs::Literal::Bool(b) => b.to_string(),
74                    litrs::Literal::Integer(i) => i.to_string(),
75                    litrs::Literal::Float(f) => f.to_string(),
76                    litrs::Literal::Char(_c) => {
77                        unimplemented!("Character literals are not implemented")
78                    }
79                    litrs::Literal::String(s) => s.into_value().into_owned(),
80                    litrs::Literal::Byte(_b) => unimplemented!("Byte literals are not implemented"),
81                    litrs::Literal::ByteString(_s) => {
82                        unimplemented!("Byte literals are not implemented")
83                    }
84                };
85                ShTokenTree::Value(value)
86            }
87        }
88    }
89}
90
91#[must_use]
92enum ParseResult {
93    KeepGoing,
94    Done(Cmd),
95}
96
97impl Default for ShParser {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl ShParser {
104    pub fn new() -> Self {
105        Self {
106            state: ParseState::Cmd,
107            cmd: None,
108            args: Vec::new(),
109            sink: None,
110            source: None,
111        }
112    }
113
114    pub fn feed(&mut self, token: TokenTree) -> ParseResult {
115        let token = ShTokenTree::from(token);
116        match self.state {
117            ParseState::Cmd => {
118                let ShTokenTree::Value(value) = token else {
119                    panic!("Unexpected command: {token:?}");
120                };
121                self.cmd = Some(value);
122                self.state = ParseState::Args;
123            }
124            ParseState::Args => match token {
125                ShTokenTree::Value(v) => self.args.push(Arg::Literal(v)),
126                ShTokenTree::EndOfLine => return ParseResult::Done(self.into_sh()),
127                ShTokenTree::Expr(g) => self.args.push(Arg::Expr(g.stream())),
128                ShTokenTree::Sink => self.state = ParseState::SetSink,
129                ShTokenTree::Source => self.state = ParseState::SetSource,
130            },
131            ParseState::SetSink => {
132                assert!(self.sink.is_none(), "Can't set the sink more than once");
133                match token {
134                    ShTokenTree::Value(v) => self.sink = Some(Sink::File(v)),
135                    ShTokenTree::Expr(g) => self.sink = Some(Sink::Expr(g.stream())),
136                    other => panic!("Unexpected token: {other:?}"),
137                }
138                self.state = ParseState::DoneSetSink;
139            }
140            ParseState::SetSource => {
141                assert!(self.source.is_none(), "Can't set the source more than once");
142                match token {
143                    ShTokenTree::Value(v) => self.source = Some(Source::File(v)),
144                    ShTokenTree::Expr(g) => {
145                        self.source = Some(Source::Expr(g.stream()));
146                    }
147                    other => panic!("Unexpected token: {other:?}"),
148                }
149                self.state = ParseState::DoneSetSource;
150            }
151            ParseState::DoneSetSink => match token {
152                ShTokenTree::EndOfLine => return ParseResult::Done(self.into_sh()),
153                ShTokenTree::Source => self.state = ParseState::SetSource,
154                other => panic!("Unexpected token: {other:?}"),
155            },
156            ParseState::DoneSetSource => match token {
157                ShTokenTree::EndOfLine => return ParseResult::Done(self.into_sh()),
158                ShTokenTree::Sink => self.state = ParseState::SetSink,
159                other => panic!("Unexpected token: {other:?}"),
160            },
161        }
162        ParseResult::KeepGoing
163    }
164
165    fn finish(mut self) -> Option<Cmd> {
166        if self.cmd.is_some() {
167            Some(self.into_sh())
168        } else {
169            None
170        }
171    }
172
173    fn into_sh(&mut self) -> Cmd {
174        let mut parser = std::mem::take(self);
175        Cmd {
176            cmd: parser.cmd.take().expect("Missing command"),
177            args: parser.args,
178            sink: parser.sink,
179            source: parser.source,
180        }
181    }
182}
183
184/// A command-running macro.
185///
186/// `cmd` is a macro for running external commands. It provides functionality to
187/// pipe the input and output to/from variables as well as using rust expressions
188/// as arguments to the program.
189///
190/// The format of a `cmd` call is like so:
191///
192/// ```ignore
193/// cmd!( [prog] [arg]* [< {inexpr}]? [> {outexpr}]? [;]? )
194/// ```
195///
196/// Or you can create multiple commands on a single block
197///
198/// ```ignore
199/// cmd! {
200///   [prog] [arg]* [< {inexpr}]? [> {outexpr}]? ;
201///   [prog] [arg]* [< {inexpr}]? [> {outexpr}]? ;
202///   [prog] [arg]* [< {inexpr}]? [> {outexpr}]? [;]?
203/// }
204/// ```
205///
206/// Arguments are allowed to take the form of identifiers (i.e. plain text),
207/// literals (numbers, quoted strings, characters, etc.), or rust expressions
208/// delimited by braces.
209///
210/// This macro doesn't execute the commands. It returns an iterator of `sh::QCmd` which
211/// can be executed. Alternatively, see `sh::sh` which executes the commands sequentially.
212///
213/// # Examples
214///
215/// ```ignore
216/// # use sh_macro::cmd;
217/// # #[cfg(target_os = "linux")]
218/// # fn run() {
219/// let world = "world";
220/// let mut out = String::new();
221/// cmd!(echo hello {world} > {&mut out}).for_each(|cmd| cmd.exec().unwrap());
222/// assert_eq!(out, "hello world\n");
223/// # }
224/// # run();
225/// ```
226///
227/// ```ignore
228/// # use sh_macro::cmd;
229/// # #[cfg(target_os = "linux")]
230/// # fn run() {
231/// cmd! {
232///   echo hello;
233///   sleep 5;
234///   echo world;
235/// }.for_each(|cmd| cmd.exec().unwrap()); // prints hello, waits 5 seconds, prints world.
236/// # }
237/// # run();
238/// ```
239///
240/// You can also use string literals as needed
241///
242/// ```ignore
243/// # use sh_macro::cmd;
244/// # #[cfg(target_os = "linux")]
245/// # fn run() {
246/// let mut out = String::new();
247/// cmd!(echo "hello world" > {&mut out}).for_each(|cmd| cmd.exec().unwrap());
248/// assert_eq!(out, "hello world\n");
249/// # }
250/// # run();
251/// ```
252#[proc_macro]
253pub fn cmd(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
254    let stream: TokenStream = stream.into();
255    let mut stream = stream.into_iter();
256    let mut cmds: Vec<Cmd> = Vec::new();
257
258    let mut parser = ShParser::new();
259    while let Some(token) = stream.next() {
260        match parser.feed(token) {
261            ParseResult::KeepGoing => {}
262            ParseResult::Done(sh) => cmds.push(sh),
263        }
264    }
265    if let Some(cmd) = parser.finish() {
266        cmds.push(cmd);
267    }
268
269    quote!(
270        {
271            let mut __commands = Vec::new();
272            #(
273                __commands.push({
274                    #cmds
275                });
276            )*
277            __commands.into_iter()
278        }
279    )
280    .into()
281}
282
283impl ToTokens for Cmd {
284    fn to_tokens(&self, tokens: &mut TokenStream) {
285        let cmd = &self.cmd;
286        let args = &self.args;
287        tokens.append_all(quote! {
288            let mut __cmd = ::std::process::Command::new(#cmd);
289            #(
290                __cmd.arg({ #args });
291            )*
292            let mut __builder = ::sh::QCmdBuilder::new(__cmd);
293        });
294        match &self.sink {
295            Some(Sink::File(_)) => {
296                unimplemented!("Writing command output to file is not yet implemented")
297            }
298            Some(Sink::Expr(expr)) => tokens.append_all(quote! {
299                __builder.sink({ #expr });
300            }),
301            None => {}
302        }
303        match &self.source {
304            Some(Source::File(_)) => {
305                unimplemented!("Reading command input from file is not yet implemented");
306            }
307            Some(Source::Expr(expr)) => tokens.append_all(quote! {
308                __builder.source({ #expr });
309            }),
310            None => {}
311        }
312        tokens.append_all(quote! {
313            __builder.build()
314        })
315    }
316}