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#[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}