use camino::{Utf8Path, Utf8PathBuf};
use miette::{miette, Context, IntoDiagnostic};
use std::{process::Command, sync::Mutex};
pub static WORKING_DIR: Mutex<String> = Mutex::new(String::new());
pub struct CommandInfo {
name: String,
cmd: String,
args: Vec<String>,
version: Option<String>,
}
impl CommandInfo {
pub fn set_working_dir(dir: &Utf8Path) {
*WORKING_DIR.lock().unwrap() = dir.to_string();
}
pub fn get_working_dir() -> Utf8PathBuf {
Utf8PathBuf::from(&*WORKING_DIR.lock().unwrap())
}
pub fn new(name: &str, path: Option<&str>) -> Option<Self> {
let cmd = path.unwrap_or(name).to_owned();
let output = Command::new(&cmd)
.arg("--version")
.current_dir(Self::get_working_dir())
.output()
.ok()?;
Some(CommandInfo {
name: name.to_owned(),
cmd,
args: vec![],
version: parse_version(output),
})
}
#[allow(dead_code)]
pub fn new_unchecked(name: &str, path: Option<&str>) -> Self {
let cmd = path.unwrap_or(name).to_owned();
CommandInfo {
name: name.to_owned(),
cmd,
args: vec![],
version: None,
}
}
pub fn new_powershell_command(name: &str) -> Option<Self> {
let output = Command::new("powershell")
.arg("-Command")
.arg("Get-Command")
.arg(name)
.current_dir(Self::get_working_dir())
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(CommandInfo {
name: name.to_owned(),
cmd: "powershell".to_owned(),
args: vec!["-Command".to_owned(), name.to_owned()],
version: parse_version(output),
})
}
pub fn output_checked(
&self,
builder: impl FnOnce(&mut Command) -> &mut Command,
) -> Result<std::process::Output, miette::Report> {
let mut command = Command::new(&self.cmd);
command.args(&self.args);
command.current_dir(Self::get_working_dir());
builder(&mut command);
let output = command
.output()
.into_diagnostic()
.wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
if output.status.success() {
Ok(output)
} else {
let mut out = String::new();
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
out.push_str("\nstdout:\n");
out.push_str(&stdout);
out.push_str("\nstderr:\n");
out.push_str(&stderr);
Err(miette!("{out}")).wrap_err_with(|| {
format!(
"\"{}\" failed ({})",
pretty_cmd(&self.name, &command),
output.status
)
})
}
}
pub fn output(
&self,
builder: impl FnOnce(&mut Command) -> &mut Command,
) -> Result<std::process::Output, miette::Report> {
let mut command = Command::new(&self.cmd);
command.current_dir(Self::get_working_dir());
command.args(&self.args);
builder(&mut command);
let output = command
.output()
.into_diagnostic()
.wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
Ok(output)
}
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
}
fn parse_version(output: std::process::Output) -> Option<String> {
let version_bytes = output.stdout;
let version_full = String::from_utf8(version_bytes).ok()?;
let version_line = version_full.lines().next()?;
let version_suffix = version_line.split_once(' ')?.1.trim().to_owned();
Some(version_suffix)
}
fn pretty_cmd(name: &str, cmd: &Command) -> String {
let mut out = String::new();
out.push_str(name);
for arg in cmd.get_args() {
out.push(' ');
out.push_str(&arg.to_string_lossy())
}
out
}