use std::{
ffi::OsStr,
path::{Path, PathBuf},
process::{Command, Stdio},
time::{Duration, Instant},
};
const TIMEOUT: Duration = Duration::from_secs(5);
const TICK: Duration = Duration::from_millis(20);
pub fn adopt() {
if cfg!(windows) {
return;
}
if std::env::var_os("TERM").is_some() {
return;
}
let Some(shell) = std::env::var_os("SHELL") else {
return;
};
let Some(found) = query(&shell) else {
return;
};
let merged = merge(&found, &std::env::var("PATH").unwrap_or_default());
unsafe { std::env::set_var("PATH", merged) };
}
fn query(shell: &OsStr) -> Option<String> {
let mut child = Command::new(shell)
.args(["-l", "-i", "-c", "/usr/bin/env"])
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.ok()?;
let deadline = Instant::now() + TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Err(_) => return None,
Ok(None) if Instant::now() >= deadline => {
let _ = child.kill();
return None;
}
Ok(None) => std::thread::sleep(TICK),
}
}
let output = child.wait_with_output().ok()?;
parse(&String::from_utf8_lossy(&output.stdout)).map(str::to_owned)
}
pub fn parse(env: &str) -> Option<&str> {
env.lines()
.rev()
.find_map(|line| line.strip_prefix("PATH="))
}
pub fn merge(shell: &str, inherited: &str) -> String {
let mut kept: Vec<&str> = Vec::new();
for entry in shell.split(':').chain(inherited.split(':')) {
if !entry.is_empty() && !kept.contains(&entry) {
kept.push(entry);
}
}
kept.join(":")
}
pub fn program(command: &str) -> PathBuf {
if !cfg!(windows) {
return PathBuf::from(command);
}
let path = std::env::var_os("PATH").unwrap_or_default();
let pathext = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
resolve(command, &path, &pathext, |candidate| candidate.is_file())
.unwrap_or_else(|| PathBuf::from(command))
}
pub fn resolve(
command: &str,
path: &OsStr,
pathext: &str,
exists: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
let name = Path::new(command);
if name.extension().is_some() {
return None;
}
let under = |base: PathBuf| {
pathext
.split(';')
.filter(|ext| !ext.is_empty())
.map(|ext| {
let mut candidate = base.clone().into_os_string();
candidate.push(ext.to_ascii_lowercase());
PathBuf::from(candidate)
})
.find(|candidate| exists(candidate))
};
match name.components().count() {
1 => std::env::split_paths(path).find_map(|dir| under(dir.join(name))),
_ => under(name.to_path_buf()),
}
}