use log::{debug, error};
use std::{
env, io,
ops::{Deref, DerefMut},
process::Stdio,
result,
string::FromUtf8Error,
};
use thiserror::Error;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("cannot run command: {1}")]
SpawnProcessError(#[source] io::Error, String),
#[error("cannot get standard input")]
GetStdinError,
#[error("cannot wait for exit status code of command: {1}")]
WaitForExitStatusCodeError(#[source] io::Error, String),
#[error("cannot get exit status code of command: {0}")]
GetExitStatusCodeNotAvailableError(String),
#[error("command {0} returned non-zero exit status code {1}: {2}")]
InvalidExitStatusCodeNonZeroError(String, i32, String),
#[error("cannot write data to standard input")]
WriteStdinError(#[source] io::Error),
#[error("cannot get standard output")]
GetStdoutError,
#[error("cannot read data from standard output")]
ReadStdoutError(#[source] io::Error),
#[error("cannot get standard error")]
GetStderrError,
#[error("cannot read data from standard error")]
ReadStderrError(#[source] io::Error),
#[error("cannot get command output")]
GetOutputError(#[source] io::Error),
#[error("cannot parse command output as string")]
ParseOutputAsUtf8StringError(#[source] FromUtf8Error),
}
pub type Result<T> = result::Result<T, Error>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Cmd {
SingleCmd(SingleCmd),
Pipeline(Pipeline),
}
impl Cmd {
pub fn replace(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
match &mut self {
Self::SingleCmd(SingleCmd(cmd)) => *cmd = cmd.replace(from.as_ref(), to.as_ref()),
Self::Pipeline(Pipeline(cmds)) => {
for SingleCmd(cmd) in cmds {
*cmd = cmd.replace(from.as_ref(), to.as_ref());
}
}
}
self
}
pub async fn run_with(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
debug!("running command: {}", self.to_string());
match self {
Self::SingleCmd(cmd) => cmd.run(input).await,
Self::Pipeline(cmds) => cmds.run(input).await,
}
}
pub async fn run(&self) -> Result<CmdOutput> {
self.run_with([]).await
}
}
impl Default for Cmd {
fn default() -> Self {
Self::Pipeline(Pipeline::default())
}
}
impl From<String> for Cmd {
fn from(cmd: String) -> Self {
Self::SingleCmd(cmd.into())
}
}
impl From<&String> for Cmd {
fn from(cmd: &String) -> Self {
Self::SingleCmd(cmd.into())
}
}
impl From<&str> for Cmd {
fn from(cmd: &str) -> Self {
Self::SingleCmd(cmd.into())
}
}
impl From<Vec<String>> for Cmd {
fn from(cmd: Vec<String>) -> Self {
Self::Pipeline(cmd.into())
}
}
impl From<Vec<&String>> for Cmd {
fn from(cmd: Vec<&String>) -> Self {
Self::Pipeline(cmd.into())
}
}
impl From<Vec<&str>> for Cmd {
fn from(cmd: Vec<&str>) -> Self {
Self::Pipeline(cmd.into())
}
}
impl From<&[String]> for Cmd {
fn from(cmd: &[String]) -> Self {
Self::Pipeline(cmd.into())
}
}
impl From<&[&String]> for Cmd {
fn from(cmd: &[&String]) -> Self {
Self::Pipeline(cmd.into())
}
}
impl From<&[&str]> for Cmd {
fn from(cmd: &[&str]) -> Self {
Self::Pipeline(cmd.into())
}
}
impl ToString for Cmd {
fn to_string(&self) -> String {
match self {
Self::SingleCmd(cmd) => cmd.to_string(),
Self::Pipeline(pipeline) => pipeline.to_string(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SingleCmd(String);
impl SingleCmd {
async fn run(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
let windows = cfg!(target_os = "windows")
&& !(env::var("MSYSTEM")
.map(|env| env.starts_with("MINGW"))
.unwrap_or_default());
let (shell, arg) = if windows { ("cmd", "/C") } else { ("sh", "-c") };
let mut cmd = Command::new(shell);
let cmd = cmd.args(&[arg, &self.0]);
if input.as_ref().is_empty() {
let output = cmd.output().await.map_err(Error::GetOutputError)?;
let code = output
.status
.code()
.ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
if code != 0 {
let cmd = self.to_string();
let err = String::from_utf8_lossy(&output.stderr).to_string();
return Err(Error::InvalidExitStatusCodeNonZeroError(cmd, code, err));
}
Ok(output.stdout.into())
} else {
let mut output = Vec::new();
let mut pipeline = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| Error::SpawnProcessError(err, self.to_string()))?;
pipeline
.stdin
.as_mut()
.ok_or(Error::GetStdinError)?
.write_all(input.as_ref())
.await
.map_err(Error::WriteStdinError)?;
let code = pipeline
.wait()
.await
.map_err(|err| Error::WaitForExitStatusCodeError(err, self.to_string()))?
.code()
.ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
if code != 0 {
let cmd = self.to_string();
let mut err = Vec::new();
pipeline
.stderr
.as_mut()
.ok_or(Error::GetStderrError)?
.read_to_end(&mut err)
.await
.map_err(Error::ReadStderrError)?;
let err = String::from_utf8_lossy(&err).to_string();
return Err(Error::InvalidExitStatusCodeNonZeroError(cmd, code, err));
}
pipeline
.stdout
.as_mut()
.ok_or(Error::GetStdoutError)?
.read_to_end(&mut output)
.await
.map_err(Error::ReadStdoutError)?;
Ok(output.into())
}
}
}
impl Deref for SingleCmd {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SingleCmd {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<String> for SingleCmd {
fn from(cmd: String) -> Self {
Self(cmd)
}
}
impl From<&String> for SingleCmd {
fn from(cmd: &String) -> Self {
Self(cmd.clone())
}
}
impl From<&str> for SingleCmd {
fn from(cmd: &str) -> Self {
Self(cmd.to_owned())
}
}
impl ToString for SingleCmd {
fn to_string(&self) -> String {
self.0.clone()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Pipeline(Vec<SingleCmd>);
impl Pipeline {
async fn run(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
let mut output = input.as_ref().to_owned();
for cmd in &self.0 {
output = cmd.run(&output).await?.0;
}
Ok(output.into())
}
}
impl Deref for Pipeline {
type Target = Vec<SingleCmd>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Pipeline {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<String>> for Pipeline {
fn from(cmd: Vec<String>) -> Self {
Self(cmd.into_iter().map(Into::into).collect())
}
}
impl From<Vec<&String>> for Pipeline {
fn from(cmd: Vec<&String>) -> Self {
Self(cmd.into_iter().map(Into::into).collect())
}
}
impl From<Vec<&str>> for Pipeline {
fn from(cmd: Vec<&str>) -> Self {
Self(cmd.into_iter().map(Into::into).collect())
}
}
impl From<&[String]> for Pipeline {
fn from(cmd: &[String]) -> Self {
Self(cmd.iter().map(Into::into).collect())
}
}
impl From<&[&String]> for Pipeline {
fn from(cmd: &[&String]) -> Self {
Self(cmd.iter().map(|cmd| (*cmd).into()).collect())
}
}
impl From<&[&str]> for Pipeline {
fn from(cmd: &[&str]) -> Self {
Self(cmd.iter().map(|cmd| (*cmd).into()).collect())
}
}
impl ToString for Pipeline {
fn to_string(&self) -> String {
self.0.iter().fold(String::new(), |s, cmd| {
if s.is_empty() {
cmd.to_string()
} else {
s + "|" + &cmd.to_string()
}
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CmdOutput(Vec<u8>);
impl CmdOutput {
pub fn to_string_lossy(&self) -> String {
String::from_utf8_lossy(self).to_string()
}
}
impl Deref for CmdOutput {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CmdOutput {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<u8>> for CmdOutput {
fn from(output: Vec<u8>) -> Self {
Self(output)
}
}
impl Into<Vec<u8>> for CmdOutput {
fn into(self) -> Vec<u8> {
self.0
}
}
impl TryInto<String> for CmdOutput {
type Error = Error;
fn try_into(self) -> result::Result<String, Self::Error> {
String::from_utf8(self.0).map_err(Error::ParseOutputAsUtf8StringError)
}
}