shellcomp 0.1.12

Shell completion installation and activation helpers for Rust CLI tools
Documentation
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};

use crate::error::{Error, Result};

#[derive(Clone, Debug)]
pub(crate) struct Environment {
    overrides: BTreeMap<String, Option<OsString>>,
    path_overrides: BTreeMap<PathBuf, bool>,
    file_overrides: BTreeMap<PathBuf, Option<Vec<u8>>>,
    dir_entries_overrides: BTreeMap<PathBuf, Vec<PathBuf>>,
    windows_override: Option<bool>,
    use_real_path_lookups: bool,
}

impl Default for Environment {
    fn default() -> Self {
        Self {
            overrides: BTreeMap::new(),
            path_overrides: BTreeMap::new(),
            file_overrides: BTreeMap::new(),
            dir_entries_overrides: BTreeMap::new(),
            windows_override: None,
            use_real_path_lookups: true,
        }
    }
}

impl Environment {
    pub(crate) fn system() -> Self {
        Self::default()
    }

    #[cfg(test)]
    pub(crate) fn test() -> Self {
        let mut env = Self {
            use_real_path_lookups: false,
            ..Self::default()
        };
        for key in [
            "HOME",
            "USERPROFILE",
            "XDG_CONFIG_HOME",
            "XDG_DATA_HOME",
            "ZDOTDIR",
            "BASH_COMPLETION_VERSINFO",
        ] {
            env.overrides.insert(key.to_owned(), None);
        }
        env
    }

    #[cfg(test)]
    pub(crate) fn with_var(mut self, key: &str, value: impl Into<OsString>) -> Self {
        self.overrides.insert(key.to_owned(), Some(value.into()));
        self
    }

    #[cfg(test)]
    pub(crate) fn without_var(mut self, key: &str) -> Self {
        self.overrides.insert(key.to_owned(), None);
        self
    }

    pub(crate) fn var_os(&self, key: &str) -> Option<OsString> {
        if let Some(value) = self.overrides.get(key) {
            return value.clone();
        }
        std::env::var_os(key)
    }

    pub(crate) fn path_exists(&self, path: &Path) -> bool {
        if let Some(contents) = self.file_overrides.get(path) {
            return contents.is_some();
        }
        if self.dir_entries_overrides.contains_key(path) {
            return true;
        }
        if let Some(exists) = self.path_overrides.get(path) {
            return *exists;
        }
        self.use_real_path_lookups && path.exists()
    }

    #[cfg(test)]
    pub(crate) fn with_existing_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.path_overrides.insert(path.into(), true);
        self
    }

    #[cfg(test)]
    pub(crate) fn without_existing_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.path_overrides.insert(path.into(), false);
        self
    }

    #[cfg(test)]
    pub(crate) fn with_file_contents(
        mut self,
        path: impl Into<PathBuf>,
        contents: impl Into<Vec<u8>>,
    ) -> Self {
        self.file_overrides
            .insert(path.into(), Some(contents.into()));
        self
    }

    #[cfg(test)]
    pub(crate) fn with_dir_entries(
        mut self,
        path: impl Into<PathBuf>,
        entries: impl IntoIterator<Item = PathBuf>,
    ) -> Self {
        self.dir_entries_overrides
            .insert(path.into(), entries.into_iter().collect());
        self
    }

    #[cfg(test)]
    pub(crate) fn without_real_path_lookups(mut self) -> Self {
        self.use_real_path_lookups = false;
        self
    }

    #[cfg(test)]
    pub(crate) fn with_windows_platform(mut self) -> Self {
        self.windows_override = Some(true);
        self
    }

    pub(crate) fn read_file_if_exists(&self, path: &Path) -> Result<Option<Vec<u8>>> {
        if let Some(contents) = self.file_overrides.get(path) {
            return Ok(contents.clone());
        }

        if !self.use_real_path_lookups && !self.is_user_scoped_path(path) {
            return Ok(None);
        }

        match std::fs::read(path) {
            Ok(contents) => Ok(Some(contents)),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(source) => Err(Error::io("read file", path, source)),
        }
    }

    pub(crate) fn read_dir_entries(&self, path: &Path) -> Result<Vec<PathBuf>> {
        if let Some(entries) = self.dir_entries_overrides.get(path) {
            return Ok(entries.clone());
        }

        if !self.use_real_path_lookups && !self.is_user_scoped_path(path) {
            return Ok(Vec::new());
        }

        match std::fs::read_dir(path) {
            Ok(entries) => entries
                .map(|entry| {
                    entry
                        .map(|entry| entry.path())
                        .map_err(|source| Error::io("read directory", path, source))
                })
                .collect(),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
            Err(source) => Err(Error::io("read directory", path, source)),
        }
    }

    fn is_user_scoped_path(&self, path: &Path) -> bool {
        [
            self.var_os("HOME").map(PathBuf::from),
            self.var_os("USERPROFILE").map(PathBuf::from),
            self.var_os("XDG_CONFIG_HOME").map(PathBuf::from),
            self.var_os("XDG_DATA_HOME").map(PathBuf::from),
            self.var_os("ZDOTDIR").map(PathBuf::from),
        ]
        .into_iter()
        .flatten()
        .any(|root| path.starts_with(root))
    }

    pub(crate) fn home_dir(&self) -> Result<PathBuf> {
        self.var_os("HOME")
            .map(PathBuf::from)
            .ok_or(Error::MissingHome)
    }

    pub(crate) fn xdg_config_home(&self) -> Result<PathBuf> {
        if let Some(path) = self.var_os("XDG_CONFIG_HOME") {
            return Ok(PathBuf::from(path));
        }
        Ok(self.home_dir()?.join(".config"))
    }

    pub(crate) fn xdg_data_home(&self) -> Result<PathBuf> {
        if let Some(path) = self.var_os("XDG_DATA_HOME") {
            return Ok(PathBuf::from(path));
        }
        Ok(self.home_dir()?.join(".local").join("share"))
    }

    pub(crate) fn zdotdir(&self) -> Result<PathBuf> {
        if let Some(path) = self.var_os("ZDOTDIR") {
            return Ok(PathBuf::from(path));
        }
        self.home_dir()
    }

    pub(crate) fn powershell_default_install_dir(&self) -> Result<PathBuf> {
        if self.is_windows_platform() {
            return Ok(self
                .powershell_home_dir()?
                .join("Documents")
                .join("PowerShell")
                .join("Completions"));
        }

        Ok(self.xdg_data_home()?.join("powershell").join("completions"))
    }

    pub(crate) fn powershell_profile_path(&self) -> Result<PathBuf> {
        if self.is_windows_platform() {
            return Ok(self
                .powershell_home_dir()?
                .join("Documents")
                .join("PowerShell")
                .join("profile.ps1"));
        }

        Ok(self
            .home_dir()?
            .join(".config")
            .join("powershell")
            .join("profile.ps1"))
    }

    fn powershell_home_dir(&self) -> Result<PathBuf> {
        self.var_os("USERPROFILE")
            .map(PathBuf::from)
            .or_else(|| self.var_os("HOME").map(PathBuf::from))
            .ok_or(Error::MissingHome)
    }

    pub(crate) fn is_windows_platform(&self) -> bool {
        self.windows_override.unwrap_or(cfg!(windows))
    }
}