mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use crate::sys::Runtime;

use super::ShellState;

fn resolve_path(cwd: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        cwd.join(path)
    }
}

pub(super) fn resolve_against_shell_cwd(state: &ShellState, path: &Path) -> PathBuf {
    resolve_path(&state.path_state.cwd, path)
}

pub(super) fn resolve_redirection_target(state: &ShellState, path: &Path) -> PathBuf {
    resolve_against_shell_cwd(state, path)
}

fn cdpath_applies(path: &str) -> bool {
    if path.is_empty() || path.starts_with('/') {
        return false;
    }
    let first = path.split('/').next().unwrap_or(path);
    first != "." && first != ".."
}

pub(super) fn resolve_cd_target(state: &ShellState, operand: &str) -> (PathBuf, bool) {
    let raw = PathBuf::from(operand);
    if !cdpath_applies(operand) {
        return (resolve_against_shell_cwd(state, &raw), false);
    }
    let Some(cdpath) = state.env_get("CDPATH") else {
        return (resolve_against_shell_cwd(state, &raw), false);
    };
    for entry in cdpath.split(':') {
        let base = if entry.is_empty() {
            state.path_state.cwd.clone()
        } else {
            resolve_against_shell_cwd(state, Path::new(entry))
        };
        let candidate = base.join(operand);
        if fs::metadata(&candidate).is_ok_and(|meta| meta.is_dir()) {
            return (candidate, !entry.is_empty());
        }
    }
    (resolve_against_shell_cwd(state, &raw), false)
}

pub(super) fn resolve_dot_path(state: &ShellState, path_var: &str, name: &str) -> PathBuf {
    if name.contains('/') {
        return resolve_against_shell_cwd(state, Path::new(name));
    }
    for dir in path_var.split(':') {
        let candidate = resolve_against_shell_cwd(state, Path::new(dir)).join(name);
        if fs::metadata(&candidate).is_ok_and(|meta| meta.is_file())
            && fs::File::open(&candidate).is_ok()
        {
            return candidate;
        }
    }
    resolve_against_shell_cwd(state, Path::new(name))
}

pub(super) fn resolve_command_path<R: Runtime + ?Sized>(
    state: &ShellState,
    runtime: &R,
    program: &str,
    path_var: &str,
) -> Result<PathBuf, io::Error> {
    runtime.resolve_command_path(program, path_var, &state.path_state.cwd)
}