argc 1.24.0

A bash cli framework, also a bash-based command runner
Documentation
use super::Runtime;

use anyhow::{anyhow, bail, Result};
use std::{env, fs, path::Path, process, thread};

#[derive(Debug, Clone, Copy, Default)]
pub struct NativeRuntime;

impl Runtime for NativeRuntime {
    fn os(&self) -> String {
        env::consts::OS.to_string()
    }

    fn shell_path(&self) -> Result<String> {
        match self.env_var("ARGC_SHELL_PATH") {
            Some(shell_path) => {
                if !self.exist_path(&shell_path) {
                    bail!("Invalid ARGC_SHELL_PATH, '{shell_path}' does not exist",);
                }
                Ok(shell_path)
            }
            None => self.bash_path().ok_or_else(|| anyhow!("Shell not found")),
        }
    }

    #[cfg(windows)]
    fn bash_path(&self) -> Option<String> {
        let bash_path = "C:\\Program Files\\Git\\bin\\bash.exe";
        if self.exist_path(bash_path) {
            return Some(bash_path.into());
        }
        let git_path = self.which("git")?;
        let git_parent_path = self.parent_path(&git_path)?;
        let bash_path = self.join_path(&self.parent_path(&git_parent_path)?, &["bin", "bash.exe"]);
        if self.exist_path(&bash_path) {
            return Some(bash_path);
        }
        let bash_path = self.join_path(&git_parent_path, &["bash.exe"]);
        if self.exist_path(&bash_path) {
            return Some(bash_path);
        }
        None
    }

    #[cfg(not(windows))]
    fn bash_path(&self) -> Option<String> {
        self.which("bash")
    }

    fn exec_bash_functions(
        &self,
        script_file: &str,
        functions: &[&str],
        args: &[String],
        envs: std::collections::HashMap<String, String>,
    ) -> Option<Vec<String>> {
        let shell = self.shell_path().ok()?;
        let shell_args = self.shell_args(&shell);
        let path_env = self.path_env_with_current_exe();
        let handles: Vec<_> = functions
            .iter()
            .map(|func| {
                let script_file = script_file.to_string();
                let args: Vec<String> = args.to_vec();
                let path_env = path_env.clone();
                let func = func.to_string();
                let shell = shell.clone();
                let shell_args = shell_args.clone();
                let envs = envs.clone();
                thread::spawn(move || {
                    process::Command::new(shell)
                        .args(shell_args)
                        .arg(&script_file)
                        .arg(Self::INTERNAL_SYMBOL)
                        .arg(&func)
                        .args(args)
                        .envs(envs)
                        .env("PATH", path_env)
                        .output()
                        .ok()
                        .map(|out| String::from_utf8_lossy(&out.stdout).to_string())
                        .unwrap_or_default()
                })
            })
            .collect();
        let result: Vec<String> = handles
            .into_iter()
            .map(|h| {
                h.join()
                    .ok()
                    .map(|v| v.trim().to_string())
                    .unwrap_or_default()
            })
            .collect();
        Some(result)
    }

    fn current_exe(&self) -> Option<String> {
        env::current_exe()
            .ok()
            .map(|path| path.to_string_lossy().into())
    }

    fn current_dir(&self) -> Option<String> {
        env::current_dir()
            .ok()
            .map(|path| path.to_string_lossy().into())
    }

    fn env_vars(&self) -> std::collections::HashMap<String, String> {
        env::vars().collect()
    }

    fn env_var(&self, name: &str) -> Option<String> {
        env::var(name).ok()
    }

    fn which(&self, name: &str) -> Option<String> {
        which::which(name)
            .ok()
            .map(|path| path.to_string_lossy().into())
    }

    fn exist_path(&self, path: &str) -> bool {
        Path::new(path).exists()
    }

    fn parent_path(&self, path: &str) -> Option<String> {
        Path::new(path)
            .parent()
            .map(|path| path.to_string_lossy().into())
    }

    fn join_path(&self, path: &str, parts: &[&str]) -> String {
        let mut path = Path::new(path).to_path_buf();
        for part in parts {
            path = path.join(part);
        }
        path.to_string_lossy().into()
    }

    fn chdir(&self, cwd: &str, cd: &str) -> Option<String> {
        let path = Path::new(cwd).join(cd).canonicalize().ok()?;
        Some(path.to_string_lossy().into())
    }

    fn metadata(&self, path: &str) -> Option<(bool, bool, bool)> {
        let mut meta = fs::symlink_metadata(path).ok()?;
        let is_symlink = meta.is_symlink();
        if is_symlink {
            meta = fs::metadata(path).ok()?;
        }
        let is_dir = meta.is_dir();
        #[cfg(target_family = "unix")]
        let is_executable = {
            use std::os::unix::fs::PermissionsExt;
            meta.permissions().mode() & 0o111 != 0
        };
        #[cfg(not(target_family = "unix"))]
        let is_executable = false;
        Some((is_dir, is_symlink, is_executable))
    }

    fn read_dir(&self, path: &str) -> Option<Vec<String>> {
        let dir = fs::read_dir(path).ok()?;
        let mut paths = vec![];
        for entry in dir {
            let entry = entry.ok()?;
            paths.push(entry.file_name().to_string_lossy().into());
        }
        Some(paths)
    }

    fn read_to_string(&self, path: &str) -> Option<String> {
        let data = fs::read_to_string(path).ok()?;
        Some(data)
    }
}