use std::path::PathBuf;
use nix::sys::wait::{WaitStatus, waitpid};
use nix::unistd::Pid;
use crate::error::{RuntimeErrorKind, ShellError};
pub fn find_in_path(cmd: &str, path_var: &str) -> Option<PathBuf> {
for dir in path_var.split(':') {
if dir.is_empty() {
continue;
}
let candidate = PathBuf::from(dir).join(cmd);
if candidate.is_file() {
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&candidate)
&& meta.permissions().mode() & 0o111 != 0
{
return Some(candidate);
}
}
}
None
}
#[derive(Debug)]
pub enum PathLookup {
Executable(PathBuf),
NotExecutable(PathBuf),
NotFound,
}
pub fn lookup_in_path(cmd: &str, path_var: &str) -> PathLookup {
let mut seen_non_exec: Option<PathBuf> = None;
for dir in path_var.split(':') {
if dir.is_empty() {
continue;
}
let candidate = PathBuf::from(dir).join(cmd);
if !candidate.is_file() {
continue;
}
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(&candidate) {
Ok(meta) if meta.permissions().mode() & 0o111 != 0 => {
return PathLookup::Executable(candidate);
}
Ok(_) => {
if seen_non_exec.is_none() {
seen_non_exec = Some(candidate);
}
}
Err(_) => continue,
}
}
match seen_non_exec {
Some(p) => PathLookup::NotExecutable(p),
None => PathLookup::NotFound,
}
}
pub fn wait_child(child: Pid) -> Result<i32, ShellError> {
match waitpid(child, None) {
Ok(WaitStatus::Exited(_, code)) => Ok(code),
Ok(WaitStatus::Signaled(_, sig, _)) => Ok(128 + sig as i32),
Ok(_) => Ok(0),
Err(e) => Err(ShellError::runtime(
RuntimeErrorKind::IoError,
format!("waitpid: {}", e),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn find_in_path_finds_sh() {
let path_var = env::var("PATH").unwrap_or_else(|_| "/bin:/usr/bin".to_string());
let result = find_in_path("sh", &path_var);
assert!(result.is_some(), "should find 'sh' in PATH");
}
#[test]
fn find_in_path_returns_none_for_nonexistent() {
let path_var = "/bin:/usr/bin";
let result = find_in_path("nonexistent_cmd_12345", path_var);
assert!(result.is_none());
}
#[test]
fn lookup_in_path_finds_executable() {
use super::PathLookup;
let path_var = env::var("PATH").unwrap_or_else(|_| "/bin:/usr/bin".to_string());
match lookup_in_path("sh", &path_var) {
PathLookup::Executable(p) => assert!(p.ends_with("sh")),
other => panic!("expected Executable, got {:?}", other),
}
}
#[test]
fn lookup_in_path_reports_not_found_for_missing() {
use super::PathLookup;
let path_var = "/bin:/usr/bin";
match lookup_in_path("definitely_not_a_real_cmd_xyz", path_var) {
PathLookup::NotFound => {}
other => panic!("expected NotFound, got {:?}", other),
}
}
#[test]
fn lookup_in_path_reports_not_executable() {
use super::PathLookup;
use std::io::Write;
let tmp = tempfile::tempdir().expect("tempdir");
let p = tmp.path().join("cmd_no_exec");
let mut f = std::fs::File::create(&p).expect("create file");
f.write_all(b"#!/bin/sh\n").expect("write file");
drop(f);
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&p).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&p, perms).unwrap();
let path_var = tmp.path().to_str().unwrap();
match lookup_in_path("cmd_no_exec", path_var) {
PathLookup::NotExecutable(found) => {
assert!(found.ends_with("cmd_no_exec"), "got: {}", found.display());
}
other => panic!("expected NotExecutable, got {:?}", other),
}
}
}