use std::ffi::{OsStr, OsString};
#[cfg(any(windows, test))]
use std::{
env,
path::{Path, PathBuf},
};
#[cfg(windows)]
const DEFAULT_PATHEXT: &str = ".COM;.EXE;.BAT;.CMD";
#[cfg(not(windows))]
pub fn resolve(name: &OsStr) -> OsString {
name.to_owned()
}
pub fn installed(name: &OsStr) -> bool {
let path = std::path::Path::new(name);
if path.components().count() > 1 {
return path.is_file();
}
#[cfg(windows)]
{
std::path::Path::new(&resolve(name)).is_file()
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
std::env::var_os("PATH").is_some_and(|paths| {
std::env::split_paths(&paths).any(|directory| {
std::fs::metadata(directory.join(name))
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
})
})
}
}
#[cfg(windows)]
pub fn resolve(name: &OsStr) -> OsString {
let path = env::var_os("PATH").unwrap_or_default();
let pathext = env::var_os("PATHEXT").unwrap_or_else(|| OsString::from(DEFAULT_PATHEXT));
search(name, &path, &pathext)
}
#[cfg(any(windows, test))]
fn search(name: &OsStr, path: &OsStr, pathext: &OsStr) -> OsString {
if name.is_empty() {
return name.to_owned();
}
let found = if names_a_path(name) {
executable(Path::new(name), pathext)
} else {
env::split_paths(path)
.filter(|directory| !directory.as_os_str().is_empty())
.find_map(|directory| executable(&directory.join(name), pathext))
};
found.map_or_else(|| name.to_owned(), PathBuf::into_os_string)
}
#[cfg(any(windows, test))]
fn names_a_path(name: &OsStr) -> bool {
Path::new(name)
.parent()
.is_some_and(|parent| !parent.as_os_str().is_empty())
}
#[cfg(any(windows, test))]
fn executable(candidate: &Path, pathext: &OsStr) -> Option<PathBuf> {
if candidate.extension().is_some() && candidate.is_file() {
return Some(candidate.to_path_buf());
}
pathext
.to_string_lossy()
.split(';')
.map(str::trim)
.filter(|extension| extension.len() > 1)
.find_map(|extension| {
let mut extended = candidate.as_os_str().to_owned();
extended.push(extension);
let extended = PathBuf::from(extended);
extended.is_file().then_some(extended)
})
}
#[cfg(test)]
mod tests {
use super::*;
const PATHEXT: &str = ".com;.exe;.bat;.cmd";
fn program(directory: &Path, name: &str) -> PathBuf {
let path = directory.join(name);
std::fs::write(&path, "program").unwrap();
path
}
fn resolved(name: &str, path: &Path) -> OsString {
search(OsStr::new(name), path.as_os_str(), OsStr::new(PATHEXT))
}
#[test]
fn finds_the_command_shim_npm_installs() {
let temporary = tempfile::tempdir().unwrap();
let shim = program(temporary.path(), "claude.cmd");
assert_eq!(resolved("claude", temporary.path()), shim.as_os_str());
}
#[test]
fn prefers_a_real_executable_to_a_shim() {
let temporary = tempfile::tempdir().unwrap();
let executable = program(temporary.path(), "codex.exe");
program(temporary.path(), "codex.cmd");
assert_eq!(resolved("codex", temporary.path()), executable.as_os_str());
}
#[test]
fn searches_path_in_order() {
let temporary = tempfile::tempdir().unwrap();
let first = temporary.path().join("first");
let second = temporary.path().join("second");
std::fs::create_dir_all(&first).unwrap();
std::fs::create_dir_all(&second).unwrap();
let winner = program(&first, "omp.exe");
program(&second, "omp.exe");
let path = env::join_paths([&first, &second]).unwrap();
assert_eq!(
search(OsStr::new("omp"), &path, OsStr::new(PATHEXT)),
winner.as_os_str()
);
}
#[test]
fn ignores_a_directory_of_the_same_name() {
let temporary = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temporary.path().join("claude.exe")).unwrap();
let shim = program(temporary.path(), "claude.cmd");
assert_eq!(resolved("claude", temporary.path()), shim.as_os_str());
}
#[test]
fn takes_an_explicit_path_as_given() {
let temporary = tempfile::tempdir().unwrap();
let executable = program(temporary.path(), "omp.exe");
let nothing = OsStr::new("");
assert_eq!(
search(executable.as_os_str(), nothing, OsStr::new(PATHEXT)),
executable.as_os_str()
);
assert_eq!(
search(
temporary.path().join("omp").as_os_str(),
nothing,
OsStr::new(PATHEXT)
),
executable.as_os_str()
);
}
#[test]
fn hands_back_a_name_nothing_matches() {
let temporary = tempfile::tempdir().unwrap();
assert_eq!(
resolved("opencode", temporary.path()),
OsStr::new("opencode")
);
assert_eq!(resolved("", temporary.path()), OsStr::new(""));
}
#[cfg(not(windows))]
#[test]
fn leaves_the_name_to_the_operating_system_off_windows() {
let temporary = tempfile::tempdir().unwrap();
program(temporary.path(), "claude");
assert_eq!(resolve(OsStr::new("claude")), OsStr::new("claude"));
}
}