repobin 0.1.0-alpha.1

Experimental repo-local Bazel command dispatcher; API and behavior may change without notice
Documentation
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellFragment {
    pub shell_name: String,
    pub config_hint: Option<String>,
    pub fragment: String,
}

pub fn bin_dir_on_path(bin_dir: &Path, path_var: Option<&OsStr>) -> bool {
    let Some(path_var) = path_var else {
        return false;
    };

    let wanted = normalize_for_compare(bin_dir);
    env::split_paths(path_var).any(|entry| normalize_for_compare(&entry) == wanted)
}

pub fn path_update_fragment(
    bin_dir: &Path,
    shell_var: Option<&OsStr>,
    home_dir: Option<&Path>,
) -> ShellFragment {
    let shell_name = shell_var
        .and_then(|value| Path::new(value).file_name())
        .map(|value| value.to_string_lossy().to_string())
        .unwrap_or_else(|| "sh".to_string());

    match shell_name.as_str() {
        "fish" => ShellFragment {
            shell_name,
            config_hint: Some("~/.config/fish/config.fish".to_string()),
            fragment: format!("fish_add_path {}", fish_quote(bin_dir)),
        },
        "bash" => ShellFragment {
            shell_name,
            config_hint: Some("~/.bashrc".to_string()),
            fragment: format!("export PATH={}:\"$PATH\"", posix_quote(bin_dir)),
        },
        "zsh" => ShellFragment {
            shell_name,
            config_hint: Some("~/.zshrc".to_string()),
            fragment: format!("export PATH={}:\"$PATH\"", posix_quote(bin_dir)),
        },
        _ => ShellFragment {
            shell_name,
            config_hint: home_dir.map(|_| "~/.profile".to_string()),
            fragment: format!("export PATH={}:\"$PATH\"", posix_quote(bin_dir)),
        },
    }
}

fn normalize_for_compare(path: &Path) -> PathBuf {
    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

fn posix_quote(path: &Path) -> String {
    let raw = path.display().to_string().replace('\'', "'\\''");
    format!("'{raw}'")
}

fn fish_quote(path: &Path) -> String {
    posix_quote(path)
}

#[cfg(test)]
mod tests {
    use std::ffi::OsStr;
    use std::ffi::OsString;
    use std::fs;
    use std::path::Path;

    use tempfile::TempDir;

    use super::{bin_dir_on_path, path_update_fragment};

    #[test]
    fn path_update_fragment_matches_shell_conventions() {
        let zsh = path_update_fragment(
            Path::new("/tmp/bin"),
            Some(OsStr::new("/bin/zsh")),
            Some(Path::new("/Users/test")),
        );
        assert_eq!(zsh.config_hint.as_deref(), Some("~/.zshrc"));
        assert_eq!(zsh.fragment, "export PATH='/tmp/bin':\"$PATH\"");

        let fish = path_update_fragment(
            Path::new("/tmp/bin"),
            Some(OsStr::new("/usr/local/bin/fish")),
            Some(Path::new("/Users/test")),
        );
        assert_eq!(
            fish.config_hint.as_deref(),
            Some("~/.config/fish/config.fish")
        );
        assert_eq!(fish.fragment, "fish_add_path '/tmp/bin'");
    }

    #[test]
    fn bin_dir_on_path_canonicalizes_entries() {
        let temp = TempDir::new().expect("tempdir");
        let actual = temp.path().join("bin");
        fs::create_dir_all(&actual).expect("create dir");
        let alias_root = temp.path().join("alias-root");
        std::os::unix::fs::symlink(temp.path(), &alias_root).expect("symlink temp dir");
        let aliased = alias_root.join("bin");

        let path_var = OsString::from(aliased.as_os_str());
        assert!(bin_dir_on_path(&actual, Some(path_var.as_os_str())));
    }
}