| //! Private implementation details of `xshell`. |
| |
| #![deny(missing_debug_implementations)] |
| #![deny(rust_2018_idioms)] |
| |
| use std::iter; |
| |
| use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree}; |
| |
| #[doc(hidden)] |
| #[proc_macro] |
| pub fn __cmd(macro_arg: TokenStream) -> TokenStream { |
| try_cmd(macro_arg).unwrap_or_else(|msg| parse_ts(&format!("compile_error!({:?})", msg))) |
| } |
| |
| type Result<T> = std::result::Result<T, String>; |
| |
| fn try_cmd(macro_arg: TokenStream) -> Result<TokenStream> { |
| let (cmd, literal) = { |
| let mut iter = macro_arg.into_iter(); |
| let cmd = iter.next().unwrap(); |
| let literal = iter.next().unwrap(); |
| assert!(iter.next().is_none()); |
| (cmd, literal) |
| }; |
| |
| let literal = match into_literal(&literal) { |
| Some(it) => it, |
| None => return Err("expected a plain string literal".to_string()), |
| }; |
| |
| let literal_text = literal.to_string(); |
| if !literal_text.starts_with('"') { |
| return Err("expected a plain string literal".to_string()); |
| } |
| |
| let mut args = shell_lex(literal_text.as_str(), literal.span()); |
| |
| let mut res = TokenStream::new(); |
| |
| { |
| let (_joined_to_prev, splat, program) = |
| args.next().ok_or_else(|| "command can't be empty".to_string())??; |
| if splat { |
| return Err("can't splat program name".to_string()); |
| } |
| res.extend(Some(cmd)); |
| res.extend(program); |
| } |
| |
| let mut prev_spat = false; |
| for arg in args { |
| let (joined_to_prev, splat, arg) = arg?; |
| if prev_spat && joined_to_prev { |
| return Err(format!( |
| "can't combine splat with concatenation, add spaces around `{{{}...}}`", |
| trim_decorations(&res.into_iter().last().unwrap().to_string()), |
| )); |
| } |
| prev_spat = splat; |
| |
| let method = match (joined_to_prev, splat) { |
| (false, false) => ".arg", |
| (false, true) => ".args", |
| (true, false) => ".__extend_arg", |
| (true, true) => { |
| return Err(format!( |
| "can't combine splat with concatenation, add spaces around `{{{}...}}`", |
| trim_decorations(&arg.to_string()), |
| )) |
| } |
| }; |
| |
| res.extend(parse_ts(method)); |
| res.extend(arg); |
| } |
| |
| Ok(res) |
| } |
| |
| fn into_literal(ts: &TokenTree) -> Option<Literal> { |
| match ts { |
| TokenTree::Literal(l) => Some(l.clone()), |
| TokenTree::Group(g) => match g.delimiter() { |
| Delimiter::None => match g.stream().into_iter().collect::<Vec<_>>().as_slice() { |
| [TokenTree::Literal(l)] => Some(l.clone()), |
| _ => None, |
| }, |
| Delimiter::Parenthesis | Delimiter::Brace | Delimiter::Bracket => None, |
| }, |
| _ => None, |
| } |
| } |
| |
| fn trim_decorations(s: &str) -> &str { |
| &s[1..s.len() - 1] |
| } |
| |
| fn shell_lex( |
| cmd: &str, |
| call_site: Span, |
| ) -> impl Iterator<Item = Result<(bool, bool, TokenStream)>> + '_ { |
| tokenize(cmd).map(move |token| { |
| let token = token?; |
| let mut splat = false; |
| let ts = match token.kind { |
| TokenKind::Word => parse_ts(&format!("(\"{}\")", token.text)), |
| TokenKind::String => parse_ts(&format!("(\"{}\")", trim_decorations(token.text))), |
| TokenKind::Interpolation { splat: s } => { |
| splat = s; |
| let text = trim_decorations(token.text); |
| let text = &text[..text.len() - (if splat { "...".len() } else { 0 })]; |
| if !(text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')) { |
| return Err(format!( |
| "can only interpolate simple variables, got this expression instead: `{}`", |
| text |
| )); |
| } |
| let ts = if splat { format!("({})", text) } else { format!("(&({}))", text) }; |
| respan(parse_ts(&ts), call_site) |
| } |
| }; |
| Ok((token.joined_to_prev, splat, ts)) |
| }) |
| } |
| |
| /// Like trim_matches except only trims a maximum of 1 match |
| fn strip_matches<'a>(s: &'a str, pattern: &str) -> &'a str { |
| s.strip_prefix(pattern).unwrap_or(s).strip_suffix(pattern).unwrap_or(s) |
| } |
| |
| fn tokenize(cmd: &str) -> impl Iterator<Item = Result<Token<'_>>> + '_ { |
| let mut cmd = strip_matches(cmd, "\""); |
| |
| iter::from_fn(move || { |
| let old_len = cmd.len(); |
| cmd = cmd.trim_start(); |
| let joined_to_prev = old_len == cmd.len(); |
| if cmd.is_empty() { |
| return None; |
| } |
| let (len, kind) = match next_token(cmd) { |
| Ok(it) => it, |
| Err(err) => { |
| cmd = ""; |
| return Some(Err(err)); |
| } |
| }; |
| let token = Token { joined_to_prev, text: &cmd[..len], kind }; |
| cmd = &cmd[len..]; |
| Some(Ok(token)) |
| }) |
| } |
| |
| #[derive(Debug)] |
| struct Token<'a> { |
| joined_to_prev: bool, |
| text: &'a str, |
| kind: TokenKind, |
| } |
| #[derive(Debug)] |
| enum TokenKind { |
| Word, |
| String, |
| Interpolation { splat: bool }, |
| } |
| |
| fn next_token(s: &str) -> Result<(usize, TokenKind)> { |
| if s.starts_with('{') { |
| let len = s.find('}').ok_or_else(|| "unclosed `{` in command".to_string())? + 1; |
| let splat = s[..len].ends_with("...}"); |
| return Ok((len, TokenKind::Interpolation { splat })); |
| } |
| if s.starts_with('\'') { |
| let len = s[1..].find('\'').ok_or_else(|| "unclosed `'` in command".to_string())? + 2; |
| return Ok((len, TokenKind::String)); |
| } |
| let len = |
| s.find(|it: char| it.is_ascii_whitespace() || it == '\'' || it == '{').unwrap_or(s.len()); |
| Ok((len, TokenKind::Word)) |
| } |
| |
| fn respan(ts: TokenStream, span: Span) -> TokenStream { |
| let mut res = TokenStream::new(); |
| for tt in ts { |
| let tt = match tt { |
| TokenTree::Ident(mut ident) => { |
| ident.set_span(ident.span().resolved_at(span).located_at(span)); |
| TokenTree::Ident(ident) |
| } |
| TokenTree::Group(group) => { |
| TokenTree::Group(Group::new(group.delimiter(), respan(group.stream(), span))) |
| } |
| _ => tt, |
| }; |
| res.extend(Some(tt)) |
| } |
| res |
| } |
| |
| fn parse_ts(s: &str) -> TokenStream { |
| s.parse().unwrap() |
| } |