use syn::parse_macro_input;
#[proc_macro]
pub fn run_command(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
detail::run_command(parse_macro_input!(input))
.unwrap_or_else(|error| error.to_compile_error())
.into()
}
#[proc_macro]
pub fn run_command_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
detail::run_command_str(parse_macro_input!(input))
.unwrap_or_else(|error| error.to_compile_error())
.into()
}
mod detail {
use std::process::Command;
use proc_macro2::Span;
use quote::quote;
use syn::{Error, Result};
pub fn run_command_str(input: ArgList) -> Result<proc_macro2::TokenStream> {
let args: Vec<_> = input.args.iter().map(|x| x.value()).collect();
let output = execute_command(Command::new(&args[0]).args(&args[1..]))?;
let output = strip_trailing_newline(output.stdout);
let output = std::str::from_utf8(&output).expect("invalid UTF-8 in command output");
Ok(quote!(#output))
}
pub fn run_command(input: ArgList) -> Result<proc_macro2::TokenStream> {
let args: Vec<_> = input.args.iter().map(|x| x.value()).collect();
let output = execute_command(Command::new(&args[0]).args(&args[1..]))?;
let output = strip_trailing_newline(output.stdout);
let output = syn::LitByteStr::new(&output, proc_macro2::Span::call_site());
Ok(quote!(#output))
}
pub struct ArgList {
args: syn::punctuated::Punctuated<syn::LitStr, syn::token::Comma>,
}
impl syn::parse::Parse for ArgList {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
type Inner = syn::punctuated::Punctuated<syn::LitStr, syn::token::Comma>;
let args = Inner::parse_terminated(input)?;
if args.is_empty() {
Err(Error::new(input.cursor().span(), "missing required argument: command"))
} else {
Ok(Self { args })
}
}
}
fn execute_command(command: &mut Command) -> Result<std::process::Output> {
let output = command
.output()
.map_err(|error| Error::new(Span::call_site(), format!("failed to execute command: {}", error)))?;
verbose_command_error(output).map_err(|message| Error::new(Span::call_site(), message))
}
fn verbose_command_error(output: std::process::Output) -> std::result::Result<std::process::Output, String> {
if output.status.success() {
Ok(output)
} else if let Some(status) = output.status.code() {
let message = Some(strip_trailing_newline(output.stderr));
let message = message.filter(|m| !m.is_empty() && m.len() <= 500);
let message = message.filter(|m| !m.iter().any(|c| c == &b'\n'));
let message = message.and_then(|m| String::from_utf8(m).ok());
if let Some(message) = message {
Err(format!("external command exited with status {}: {}", status, message))
} else {
Err(format!("external command exited with status {}", status))
}
} else {
#[cfg(target_family = "unix")]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = output.status.signal() {
Err(format!("external command killed by signal {}", signal))
} else {
Err("external command failed, but did not exit and was not killed by a signal, this can only be a bug in std::process".into())
}
}
#[cfg(not(target_family = "unix"))]
{
Err(format!("external command killed by signal"))
}
}
}
fn strip_trailing_newline(mut input: Vec<u8>) -> Vec<u8> {
if !input.is_empty() && input[input.len() - 1] == b'\n' {
input.pop();
}
input
}
}