use std::path::Path;
use pmpx_plugin::CommandSpec;
use crate::error::{PmpxError, Result};
use crate::style;
mod command;
mod not_found;
mod quoting;
mod resolve;
pub use command::command_for;
pub fn announce(spec: &CommandSpec) {
let mut line = format!(
"{} {}",
style::paint(style::DIM, "pmpx ->"),
style::paint(style::PM, spec.program.to_string_lossy())
);
for arg in &spec.args {
line.push(' ');
line.push_str(&arg.to_string_lossy());
}
anstream::eprintln!("{line}");
}
pub fn run(spec: &CommandSpec, cwd: &Path) -> Result<u8> {
let mut cmd = command_for(spec, cwd)?;
let status = cmd.status().map_err(|e| {
PmpxError::Other(anyhow::anyhow!(e).context(format!(
"failed to start {}",
spec.program.to_string_lossy()
)))
})?;
Ok(exit_code_of(status))
}
fn exit_code_of(status: std::process::ExitStatus) -> u8 {
if let Some(code) = status.code() {
if (0..=255).contains(&code) {
return code as u8;
}
error_line(format!(
"backend exit code {code} is outside 0-255, passing through the low 8 bits"
));
return (code & 0xFF) as u8;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
error_line(format!("the backend was killed by signal {sig}"));
return (128 + sig).clamp(0, 255) as u8;
}
}
error_line("cannot read the backend exit code, treating it as 1");
1
}
fn error_line(body: impl std::fmt::Display) {
anstream::eprintln!(
"{} {}",
style::paint(style::ERROR, "pmpx:"),
style::paint(style::ERROR_BODY, body)
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runs_a_native_command_and_returns_its_exit_code() {
let tmp = tempfile::tempdir().unwrap();
let spec = CommandSpec::new("cargo").arg("--version");
assert_eq!(
run(&spec, tmp.path()).unwrap(),
0,
"cargo --version should succeed"
);
}
#[test]
fn a_nonzero_backend_exit_code_is_passed_through() {
let tmp = tempfile::tempdir().unwrap();
#[cfg(windows)]
let spec = CommandSpec::new("cmd").arg("/c").arg("exit 7");
#[cfg(not(windows))]
let spec = CommandSpec::new("sh").arg("-c").arg("exit 7");
assert_eq!(
run(&spec, tmp.path()).unwrap(),
7,
"must pass through verbatim"
);
}
#[test]
fn the_working_directory_is_honoured() {
let tmp = tempfile::tempdir().unwrap();
let marker = tmp.path().join("pmpx-cwd-probe.txt");
std::fs::write(&marker, "here").unwrap();
let spec = CommandSpec::new("cargo").arg("--version");
let mut cmd = command_for(&spec, tmp.path()).unwrap();
let out = cmd.output().unwrap();
assert!(out.status.success());
}
#[cfg(windows)]
#[test]
fn a_cmd_shim_really_runs_and_receives_its_args() {
let tmp = tempfile::tempdir().unwrap();
let out_file = tmp.path().join("got.txt");
let shim = tmp.path().join("probe.cmd");
std::fs::write(
&shim,
format!("@echo off\r\necho %1 %2 > \"{}\"\r\n", out_file.display()),
)
.unwrap();
let spec = CommandSpec::new(shim.as_os_str()).arg("add").arg("serde");
assert_eq!(run(&spec, tmp.path()).unwrap(), 0);
let got = std::fs::read_to_string(&out_file).unwrap();
assert_eq!(got.trim(), "add serde");
}
#[cfg(windows)]
#[test]
fn a_cmd_shim_in_a_path_with_spaces_still_runs() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("a dir with spaces");
std::fs::create_dir_all(&dir).unwrap();
let shim = dir.join("probe.cmd");
std::fs::write(&shim, "@echo off\r\nexit 0\r\n").unwrap();
let spec = CommandSpec::new(shim.as_os_str());
assert_eq!(
run(&spec, tmp.path()).unwrap(),
0,
"a path with spaces must work -- that is why the double quoting exists"
);
}
#[cfg(windows)]
#[test]
fn a_cmd_shim_receives_an_argument_with_spaces() {
let tmp = tempfile::tempdir().unwrap();
let out_file = tmp.path().join("got.txt");
let shim = tmp.path().join("probe.cmd");
std::fs::write(
&shim,
format!("@echo off\r\necho %~1 > \"{}\"\r\n", out_file.display()),
)
.unwrap();
let spec = CommandSpec::new(shim.as_os_str()).arg("hello world");
assert_eq!(run(&spec, tmp.path()).unwrap(), 0);
let got = std::fs::read_to_string(&out_file).unwrap();
assert_eq!(got.trim(), "hello world");
}
#[cfg(windows)]
#[test]
fn a_cmd_shim_passes_through_a_nonzero_exit_code() {
let tmp = tempfile::tempdir().unwrap();
let shim = tmp.path().join("probe.cmd");
std::fs::write(&shim, "@echo off\r\nexit /b 42\r\n").unwrap();
let spec = CommandSpec::new(shim.as_os_str());
assert_eq!(run(&spec, tmp.path()).unwrap(), 42);
}
}