1use anyhow::{Context, Result, bail};
12use std::path::{Path, PathBuf};
13
14pub fn resolve_command(command: &str) -> Result<PathBuf> {
23 let p = Path::new(command);
24 if p.is_absolute() || command.contains('/') || command.contains('\\') {
25 return p
26 .canonicalize()
27 .with_context(|| format!("canonicalize {command}"));
28 }
29 let path_var = std::env::var_os("PATH")
30 .ok_or_else(|| anyhow::anyhow!("PATH env var unset; cannot resolve `{command}`"))?;
31 for dir in std::env::split_paths(&path_var) {
32 let candidate = dir.join(command);
33 if candidate.is_file() {
34 return candidate
35 .canonicalize()
36 .with_context(|| format!("canonicalize {}", candidate.display()));
37 }
38 #[cfg(target_os = "windows")]
39 {
40 let with_exe = dir.join(format!("{command}.exe"));
41 if with_exe.is_file() {
42 return with_exe
43 .canonicalize()
44 .with_context(|| format!("canonicalize {}", with_exe.display()));
45 }
46 }
47 }
48 bail!("could not find `{command}` on PATH");
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn errors_on_missing_binary() {
57 assert!(resolve_command("definitely-not-a-real-binary-xyz123").is_err());
58 }
59
60 #[cfg(unix)]
61 #[test]
62 fn resolves_bare_program_on_path_to_absolute() {
63 let resolved = resolve_command("sh").expect("sh is on PATH");
66 assert!(
67 resolved.is_absolute(),
68 "expected absolute, got {resolved:?}"
69 );
70 assert!(resolved.exists());
71 }
72
73 #[test]
74 fn absolute_path_is_canonicalized() {
75 let tmp = tempfile::NamedTempFile::new().unwrap();
76 let resolved = resolve_command(tmp.path().to_str().unwrap()).unwrap();
77 assert!(resolved.is_absolute());
78 }
79}