use std::process::Command;
pub(crate) fn command(name: &str) -> Command {
#[cfg(windows)]
{
let path = std::env::var_os("PATH").unwrap_or_default();
let pathext = std::env::var_os("PATHEXT").unwrap_or_else(|| ".COM;.EXE;.BAT;.CMD".into());
if let Some(resolved) = resolve_windows(name, &path, &pathext) {
return Command::new(resolved);
}
}
Command::new(name)
}
#[cfg(any(windows, test))]
fn resolve_windows(
name: &str,
path: &std::ffi::OsStr,
pathext: &std::ffi::OsStr,
) -> Option<std::path::PathBuf> {
use std::path::Path;
if name.is_empty() || Path::new(name).components().count() > 1 {
return None;
}
let pathext = pathext.to_string_lossy();
let exts: Vec<&str> = pathext.split(';').filter(|ext| !ext.is_empty()).collect();
let has_explicit_extension = Path::new(name).extension().is_some();
for dir in std::env::split_paths(path) {
if has_explicit_extension {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
continue;
}
for ext in &exts {
let candidate = dir.join(format!("{name}{ext}"));
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::fs;
use std::path::PathBuf;
use super::{command, resolve_windows};
use crate::tool::test_support::TempDir;
#[test]
fn command_passthrough_when_unresolved() {
let cmd = command("definitely-not-on-path-xyz");
assert_eq!(cmd.get_program(), "definitely-not-on-path-xyz");
}
#[test]
fn resolve_finds_cmd_shim_via_pathext() {
let dir = TempDir::new("program-resolve");
let shim = dir.path().join("foo.cmd");
fs::write(&shim, "@echo off\n").expect("shim should be written");
let path = OsString::from(dir.path());
let pathext = OsString::from(".com;.exe;.bat;.cmd");
let resolved = resolve_windows("foo", &path, &pathext);
assert_eq!(resolved, Some(shim));
}
#[test]
fn resolve_prefers_earlier_path_entry() {
let first = TempDir::new("program-first");
let second = TempDir::new("program-second");
let preferred = first.path().join("foo.cmd");
let fallback = second.path().join("foo.cmd");
fs::write(&preferred, "@echo off\n").expect("first shim should be written");
fs::write(&fallback, "@echo off\n").expect("second shim should be written");
let mut joined = OsString::from(first.path());
joined.push(if cfg!(windows) { ";" } else { ":" });
joined.push(second.path());
let pathext = OsString::from(".cmd");
let resolved = resolve_windows("foo", &joined, &pathext);
assert_eq!(resolved, Some(preferred));
}
#[test]
fn resolve_returns_none_when_missing() {
let dir = TempDir::new("program-miss");
let path = OsString::from(dir.path());
let pathext = OsString::from(".com;.exe;.bat;.cmd");
let resolved = resolve_windows("ghost", &path, &pathext);
assert_eq!(resolved, None);
}
#[test]
fn resolve_skips_names_with_separators() {
let dir = TempDir::new("program-abs");
let path = OsString::from(dir.path());
let pathext = OsString::from(".cmd");
let with_sep = if cfg!(windows) {
r"C:\Windows\System32\where.exe"
} else {
"/usr/bin/env"
};
assert_eq!(resolve_windows(with_sep, &path, &pathext), None);
}
#[test]
fn resolve_matches_name_with_explicit_extension() {
let dir = TempDir::new("program-explicit-ext");
let shim = dir.path().join("foo.cmd");
fs::write(&shim, "@echo off\n").expect("shim should be written");
let path = OsString::from(dir.path());
let pathext = OsString::from(".cmd");
let resolved = resolve_windows("foo.cmd", &path, &pathext);
assert_eq!(resolved, Some(shim));
}
#[test]
fn resolve_explicit_extension_skips_pathext() {
let first = TempDir::new("program-explicit-first");
let second = TempDir::new("program-explicit-second");
let decoy = first.path().join("foo.cmd.exe");
let exact = second.path().join("foo.cmd");
fs::write(&decoy, "@echo off\n").expect("decoy should be written");
fs::write(&exact, "@echo off\n").expect("exact match should be written");
let mut joined = OsString::from(first.path());
joined.push(if cfg!(windows) { ";" } else { ":" });
joined.push(second.path());
let pathext = OsString::from(".exe;.cmd");
let resolved = resolve_windows("foo.cmd", &joined, &pathext);
assert_eq!(resolved, Some(exact));
}
#[test]
fn resolve_handles_empty_name() {
let pathext = OsString::from(".cmd");
let path = OsString::new();
assert_eq!(resolve_windows("", &path, &pathext), None);
}
#[test]
fn resolved_path_round_trips_through_command() {
let dir = TempDir::new("program-roundtrip");
let shim = dir.path().join("foo.cmd");
fs::write(&shim, "@echo off\n").expect("shim should be written");
let path = OsString::from(dir.path());
let pathext = OsString::from(".cmd");
let resolved = resolve_windows("foo", &path, &pathext).expect("foo.cmd should resolve");
let cmd = std::process::Command::new(&resolved);
assert_eq!(PathBuf::from(cmd.get_program()), shim);
}
}