use std::path::{Path, PathBuf};
use crate::path_entry::PathEntry;
pub fn split_path(path_value: &str) -> Vec<PathEntry> {
let sep = if cfg!(windows) { ';' } else { ':' };
path_value
.split(sep)
.filter(|s| !s.is_empty())
.map(|raw| PathEntry::from_raw(raw, |v| std::env::var(v).ok()))
.collect()
}
pub fn resolve(command: &str, path_entries: &[PathEntry]) -> Option<PathBuf> {
let exts = pathext_list();
for entry in path_entries {
let dir = Path::new(&entry.expanded);
if !dir.is_dir() {
continue;
}
if let Some(found) = probe(dir, command, &exts) {
return Some(found);
}
}
None
}
fn probe(dir: &Path, command: &str, exts: &[String]) -> Option<PathBuf> {
let already_has_ext = command.contains('.');
if cfg!(windows) {
if already_has_ext {
let candidate = dir.join(command);
if candidate.is_file() {
return Some(candidate);
}
}
for ext in exts {
let mut name = command.to_string();
if !ext.is_empty() {
name.push_str(ext);
}
let candidate = dir.join(&name);
if candidate.is_file() {
return Some(candidate);
}
}
None
} else {
let candidate = dir.join(command);
if is_executable_file(&candidate) {
Some(candidate)
} else {
None
}
}
}
#[cfg(windows)]
fn pathext_list() -> Vec<String> {
crate::expand::pathext_raw(|v| std::env::var(v).ok())
}
#[cfg(not(windows))]
fn pathext_list() -> Vec<String> {
Vec::new()
}
#[cfg(unix)]
fn is_executable_file(p: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(p) {
Ok(md) => md.is_file() && (md.permissions().mode() & 0o111) != 0,
Err(_) => false,
}
}
#[cfg(not(any(unix, windows)))]
fn is_executable_file(p: &Path) -> bool {
p.is_file()
}
#[cfg(windows)]
#[allow(dead_code)]
fn is_executable_file(p: &Path) -> bool {
p.is_file()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_path_handles_empty_entries() {
let sep = if cfg!(windows) { ';' } else { ':' };
let s = format!("a{sep}{sep}b");
let parts = split_path(&s);
let raw: Vec<&str> = parts.iter().map(|e| e.raw.as_str()).collect();
assert_eq!(raw, vec!["a", "b"]);
}
#[test]
fn missing_command_returns_none() {
let dir = std::env::temp_dir();
let entry =
PathEntry::from_raw(dir.to_string_lossy().into_owned(), |_| -> Option<String> {
None
});
let result = resolve("pathlint_definitely_no_such_command_xyz", &[entry]);
assert!(result.is_none());
}
}