use crate::{Duration, Env, Error, ErrorKind, Paths, Result, Status};
use chrono::prelude::*;
use colored::*;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::time::Instant;
#[derive(
Serialize, Deserialize, Clone, Debug, Hash, Default, PartialEq, Eq, juniper::GraphQLObject,
)]
pub struct Command {
pub dir: String,
pub path: String,
pub name: String,
pub args: Vec<String>,
pub stdout: String,
pub stderr: String,
pub status: Status,
pub exit_code: i32,
pub duration: Duration,
pub timeout: Duration,
pub start: String,
pub env: Env,
pub tags: Vec<String>,
}
impl Command {
pub fn new(command: &str) -> Command {
let (name, args) = crate::text::parse_command(command);
let mut c = Command::default();
c.timeout = Duration::from_std(std::time::Duration::new(300, 0)).unwrap();
c.name = name;
c.args = args;
c.dir = match std::env::current_dir() {
Ok(dir) => dir.to_str().unwrap().to_string(),
Err(_) => std::env::temp_dir().to_str().unwrap().to_string(),
};
c
}
pub fn start_utc(&self) -> DateTime<Utc> {
match &self.start.parse::<DateTime<Utc>>() {
Ok(dt) => *dt,
Err(_) => Utc::now(),
}
}
pub fn age(&self) -> std::time::Duration {
let chrono_duration = Utc::now()
.naive_utc()
.signed_duration_since(self.start_utc().naive_utc());
match chrono_duration.to_std() {
Ok(duration) => duration,
Err(_) => std::time::Duration::new(0, 0),
}
}
pub fn get_tag_value(&self, name: &str) -> Option<String> {
for tag in &self.tags {
let prop_name = format!("{}=", name);
if tag.contains(&prop_name) {
return Some(tag.replace(&prop_name, ""));
}
}
None
}
pub fn duration_string(&self) -> String {
crate::duration::format(&self.duration.to_std().unwrap())
}
pub fn summary(&self) -> String {
format!(
"{} {} {} {} ({})",
&self.status.symbol(),
&self.duration_string(),
&self.name,
&self.args.join(" "),
&self.dir
)
}
pub fn matches(&self, pattern: &str) -> bool {
if self.dir.contains(pattern) {
return true;
}
if self.stdout.contains(pattern) {
return true;
}
if self.stderr.contains(pattern) {
return true;
}
for tag in &self.tags {
if tag.contains(pattern) {
return true;
}
}
false
}
pub fn details(&self) -> String {
format!(
"{}\ndirectory: {}\nstart time: {}\noutput:\n{}\n{}",
self.summary(),
self.dir,
self.start,
self.stdout,
self.stderr
)
}
pub fn exec_in<P: AsRef<Path>>(&self, path: P) -> Result<Command> {
let mut c = self.clone();
c.dir = path
.as_ref()
.to_path_buf()
.into_os_string()
.into_string()
.unwrap();
c.exec()
}
pub fn expect_exit_code(&self, exit_code: i32) -> Result<&Command> {
if self.exit_code == exit_code {
Ok(self) } else {
Err(Error::from_kind(ErrorKind::IncorrectExitCode(
exit_code,
format!("{}", &self),
)))
}
}
pub fn exec(&self) -> Result<Command> {
let mut command = self.clone();
let now = Instant::now();
command.env = Env::default();
let mut dir = PathBuf::new();
dir.push(&self.dir);
if !dir.exists() {
return Err(Error::from_kind(ErrorKind::PathDoesNotExist(dir)));
}
let command_path = Env::which(&command.name)?;
command.path = format!("{}", Paths::which(&command.name).unwrap().display());
let mut process_command = std::process::Command::new(&command.path);
command.start = Utc::now().to_string();
process_command.current_dir(&command.dir);
let ext = Paths::extension(&command_path);
if ext == "bat" || ext == "cmd" {
command.path = format!("{}", Paths::which("cmd").unwrap().display());
process_command =
std::process::Command::new(&format!("{}", Paths::which("cmd").unwrap().display()));
process_command.current_dir(&command.dir);
let mut args: Vec<String> = Vec::new();
args.push("/C".to_string());
args.push(format!("{}", command_path.display()));
args.append(&mut command.args.clone());
process_command.args(crate::text::get_vec_osstring(&args.clone()));
} else {
process_command.current_dir(&command.dir);
process_command.args(crate::text::get_vec_osstring(&command.args.clone()));
}
command.status = Status::Ok;
command.exit_code = 0;
let output = process_command.output()?;
command.exit_code = match output.status.code() {
Some(code) => code,
None => 0,
};
command.stdout = match std::str::from_utf8(&output.stdout) {
Ok(text) => text.trim().to_string(),
Err(_) => "* error parsing stdout *".to_string(),
};
command.stderr = match std::str::from_utf8(&output.stderr) {
Ok(text) => text.trim().to_string(),
Err(_) => "* error parsing stderr *".to_string(),
};
command.status = match output.status.success() {
true => Status::Ok,
false => Status::Error,
};
command.duration = Duration::from_std(now.elapsed()).unwrap();
Ok(command)
}
}
impl Display for Command {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let mut output_lines = Vec::new();
if self.stdout.len() > 0 {
output_lines.append(&mut self.stdout.split('\n').collect());
}
if self.stderr.len() > 0 {
output_lines.append(&mut self.stderr.split('\n').collect());
}
write!(
f,
"{}{}{}{} {}\n",
&self.dir.white().bold(),
">".white().bold(),
"".clear(),
&self.name.green(),
&self.args.join(" "),
)?;
for line in output_lines {
writeln!(f, " {} {}", "".cyan(), line.clear())?;
}
Ok(())
}
}
impl PartialOrd for Command {
fn partial_cmp(&self, other: &Command) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Command {
fn cmp(&self, other: &Command) -> Ordering {
self.start_utc().cmp(&other.start_utc())
}
}
#[cfg(test)]
#[test]
fn usage() {
let git_version = Command::new("git --version").exec().unwrap();
assert_eq!(git_version.status, Status::Ok);
assert!(git_version.stdout.contains("git version"));
let dir = PathBuf::from(&git_version.dir);
assert!(dir.exists(), "dir");
}