use crate::util::{errors::Res, fmt_output, read2};
use failure::{bail, ResultExt};
use serde::{
de::{Deserialize, Deserializer, Error},
ser::{Serialize, Serializer},
};
use std::{
borrow::Cow,
fmt::Display,
process::{Command, ExitStatus, Output, Stdio},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Verbosity {
None,
Quiet,
Normal,
Verbose,
}
impl Serialize for Verbosity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match *self {
Verbosity::None => "none",
Verbosity::Quiet => "quiet",
Verbosity::Normal => "normal",
Verbosity::Verbose => "verbose",
})
}
}
impl<'de> Deserialize<'de> for Verbosity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"none" => Ok(Verbosity::None),
"quiet" => Ok(Verbosity::Quiet),
"normal" => Ok(Verbosity::Normal),
"verbose" => Ok(Verbosity::Verbose),
_ => Err(Error::custom(
r#"invalid verbosity: must be one of: none, quiet, normal, verbose"#.to_string(),
)),
}
}
}
impl Default for Verbosity {
fn default() -> Self {
Verbosity::Normal
}
}
#[derive(Debug, Clone, Copy)]
pub struct Shell {
pub verbosity: Verbosity,
}
impl Default for Shell {
fn default() -> Self {
Shell {
verbosity: Verbosity::Normal,
}
}
}
impl Shell {
pub fn println(self, status: impl Display, message: impl Display, min_verbosity: Verbosity) {
if self.verbosity >= min_verbosity {
println!("{:>12} {}", status, message);
}
}
pub fn println_unindented(
self,
status: impl Display,
message: impl Display,
min_verbosity: Verbosity,
) {
if self.verbosity >= min_verbosity {
println!("{} {}", status, message);
}
}
pub fn println_plain(self, message: impl Display, min_verbosity: Verbosity) {
if self.verbosity >= min_verbosity {
println!("{}", message);
}
}
pub fn print_plain(self, message: impl Display, min_verbosity: Verbosity) {
if self.verbosity >= min_verbosity {
print!("{}", message);
}
}
pub fn println_empty(self, min_verbosity: Verbosity) {
if self.verbosity >= min_verbosity {
println!();
}
}
}
#[derive(Default)]
pub struct OutputGroup(pub Vec<Output>);
impl OutputGroup {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, out: Output) {
self.0.push(out);
}
pub fn stdout(&self) -> impl Iterator<Item = Cow<'_, str>> {
self.0.iter().map(|x| String::from_utf8_lossy(&x.stdout))
}
pub fn stderr(&self) -> impl Iterator<Item = Cow<'_, str>> {
self.0.iter().map(|x| String::from_utf8_lossy(&x.stderr))
}
pub fn statuses(&self) -> Vec<ExitStatus> {
self.0.iter().map(|x| x.status).collect()
}
pub fn status(&self) -> Option<usize> {
for (i, o) in self.0.iter().enumerate() {
if !o.status.success() {
return Some(i);
}
}
None
}
}
impl From<Output> for OutputGroup {
fn from(f: Output) -> OutputGroup {
OutputGroup(vec![f])
}
}
pub trait CommandExt {
fn exec_streaming(
&mut self,
on_stdout_line: &mut impl FnMut(&str) -> Res<()>,
on_stderr_line: &mut impl FnMut(&str) -> Res<()>,
capture_output: bool,
) -> Res<Output>;
}
impl CommandExt for Command {
fn exec_streaming(
&mut self,
on_stdout_line: &mut impl FnMut(&str) -> Res<()>,
on_stderr_line: &mut impl FnMut(&str) -> Res<()>,
capture_output: bool,
) -> Res<Output> {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
self.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
let mut callback_error = None;
let status = (|| {
let mut child = self.spawn()?;
let out = child.stdout.take().unwrap();
let err = child.stderr.take().unwrap();
read2(out, err, &mut |is_out, data, eof| {
let idx = if eof {
data.len()
} else {
match data.iter().rposition(|b| *b == b'\n') {
Some(i) => i + 1,
None => return,
}
};
{
let new_lines = if capture_output {
let dst = if is_out { &mut stdout } else { &mut stderr };
let start = dst.len();
let data = data.drain(..idx);
dst.extend(data);
&dst[start..]
} else {
&data[..idx]
};
for line in String::from_utf8_lossy(new_lines).lines() {
if callback_error.is_some() {
break;
}
let callback_result = if is_out {
on_stdout_line(line)
} else {
on_stderr_line(line)
};
if let Err(e) = callback_result {
callback_error = Some(e);
}
}
}
if !capture_output {
data.drain(..idx);
}
})?;
child.wait()
})()
.context(format!("could not execute process {:?}", self))?;
let output = Output {
stdout,
stderr,
status,
};
{
if let Some(e) = callback_error {
return Err(e);
} else if !output.status.success() {
bail!(
"process didn't exit successfully: {:?} (code {:?})\n{})",
self,
Some(output.status),
if capture_output {
fmt_output(&output)
} else {
String::new()
},
)
}
}
Ok(output)
}
}