shell_exec 0.2.1

Cross platform library to execute shell scripts
Documentation
use std::io::Write;

use tempfile::TempPath;

use crate::{Argument, Result, Shell, ShellError};

pub enum Script {
    Inline { raw: String, shell: Shell },
    File(TempPath),
}

impl Script {
    pub async fn build(shell: Shell, cmd: String, init: Option<String>) -> Result<Self> {
        let init_script = match init.as_ref().map(|s| s.trim()) {
            Some(init) if !init.is_empty() => init_line(init, shell),
            _ => match shell {
                Shell::Bash => init_line("source ~/.bashrc", shell),
                Shell::Zsh => init_line("source ~/.zshrc", shell),
                Shell::Cmd => "@echo off".to_string(),
                _ => "".to_string(),
            },
        };

        let raw = fix_newlines(shell, &format!("{init_script}\n{cmd}"));

        let cmd = match shell {
            Shell::Cmd => {
                let file = write_file(raw).await?;
                Self::File(file)
            }
            _ => Self::Inline { raw, shell },
        };
        Ok(cmd)
    }

    pub fn argument(&self) -> Argument<'_> {
        match self {
            Script::Inline { raw, shell } => match shell {
                Shell::Wsl => Argument::Raw(raw),
                _ => Argument::Normal(raw),
            },
            Script::File(path) => Argument::Path(path.as_os_str()),
        }
    }
}

fn init_line(script: &str, shell: Shell) -> String {
    match shell {
        Shell::Cmd => format!("{script} 2> nul"),
        Shell::Powershell => format!("{script} 2>$null"),
        Shell::Bash | Shell::Zsh | Shell::Sh | Shell::Wsl => format!("{script} > /dev/null 2>&1"),
    }
}

fn fix_newlines(shell: Shell, script: &str) -> String {
    let separator = match shell {
        Shell::Cmd | Shell::Powershell => "\r\n",
        _ => "\n",
    };
    script.lines().collect::<Vec<&str>>().join(separator)
}

async fn write_file(raw: String) -> Result<TempPath> {
    tokio::task::spawn_blocking(move || {
        let mut file = tempfile::Builder::new().suffix(".bat").tempfile()?;
        file.write_all(raw.as_bytes())?;
        let a = file.into_temp_path();
        Ok(a)
    })
    .await
    .map_err(ShellError::FailedJoin)?
    .map_err(ShellError::FailedPrepare)
}