exec-target 0.3.0

the simple invoke command for test
Documentation
/*!
the simple invoke command for test

This invokes external a command and manipulates standard in out.
You can use `std::process::Command` more easily.

# Features

- minimum support rustc 1.58.0 (02072b482 2022-01-11)

# Reproducible Tests and Environment

To ensure reproducible test results across different environments, `exec-target` is opinionated about environment variables.
By default, it clears the environment and only inherits a minimal set of essential variables:
- `TERM`
- `TZ`
- `PATH`
- `LD_LIBRARY_PATH`

Additionally, it explicitly sets `LANG=C` to avoid localized output from commands (e.g., error messages in different languages), which would otherwise make string assertions fragile.

# Example

```rust
use exec_target::exec_target_with_env_in;

let command = "target/debug/exe-stab-grep";
let args = &["--color=always", "-e", "c"];
let envs = vec![("GREP_COLORS", "ms=01;32")];
let inp = b"abcdefg\n" as &[u8];

let oup = exec_target_with_env_in(command, args, envs, inp).unwrap();

assert_eq!(oup.stderr, "");
assert_eq!(oup.stdout, "ab\u{1b}[01;32m\u{1b}[Kc\u{1b}[m\u{1b}[Kdefg\n");
assert_eq!(oup.status.success(), true);
```
*/
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::process::{Command, ExitStatus, Output, Stdio};

// trats
use std::io::Write;
use std::iter::IntoIterator;

//
/// Captured output from an executed command.
///
/// Both standard output and standard error are captured and converted
/// to `String` using [`String::from_utf8_lossy`].
pub struct OutputString {
    /// The exit status of the command.
    pub status: ExitStatus,
    /// The captured standard output (stdout) as a string.
    pub stdout: String,
    /// The captured standard error (stderr) as a string.
    pub stderr: String,
}

/// Error type for command execution.
#[derive(Debug)]
pub enum ExecError {
    /// A general I/O error occurred during the process execution or piping.
    Io(std::io::Error),
    /// Failed to spawn the command. Includes the name of the executable.
    SpawnFailed(String, std::io::Error),
}

impl std::fmt::Display for ExecError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ExecError::Io(err) => write!(f, "IO error: {}", err),
            ExecError::SpawnFailed(cmd, err) => write!(f, "failed to spawn {}: {}", cmd, err),
        }
    }
}

impl std::error::Error for ExecError {}

impl From<std::io::Error> for ExecError {
    fn from(err: std::io::Error) -> Self {
        ExecError::Io(err)
    }
}

pub type Result<T> = std::result::Result<T, ExecError>;

fn setup_envs<I, K, V>(cmd: &mut Command, vars: I) -> &mut Command
where
    I: IntoIterator<Item = (K, V)>,
    K: AsRef<OsStr>,
    V: AsRef<OsStr>,
{
    let filtered_env: HashMap<String, String> = env::vars()
        .filter(|(k, _)| k == "TERM" || k == "TZ" || k == "PATH" || k == "LD_LIBRARY_PATH")
        .collect();
    cmd.env_clear()
        .envs(filtered_env)
        .envs(vars)
        .env("LANG", "C")
}

fn exec_internal<I, S, IKV, K, V>(
    target_exe: &str,
    args: I,
    env: IKV,
    in_bytes: Option<&[u8]>,
) -> Result<OutputString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
    IKV: IntoIterator<Item = (K, V)>,
    K: AsRef<OsStr>,
    V: AsRef<OsStr>,
{
    let mut cmd: Command = Command::new(target_exe);
    setup_envs(&mut cmd, env).args(args);

    if in_bytes.is_some() {
        cmd.stdin(Stdio::piped());
    }
    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());

    let mut child = cmd
        .spawn()
        .map_err(|e| ExecError::SpawnFailed(target_exe.to_string(), e))?;

    if let Some(bytes) = in_bytes {
        let stdin = child
            .stdin
            .as_mut()
            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "failed to get stdin"))?;
        let r = stdin.write_all(bytes);
        match r {
            Err(ioe) if ioe.kind() == std::io::ErrorKind::BrokenPipe => {
                // nothing todo
            }
            _ => {
                r?;
            }
        }
    }

    let output: Output = child.wait_with_output()?;
    //
    Ok(OutputString {
        status: output.status,
        stdout: String::from(String::from_utf8_lossy(&output.stdout)),
        stderr: String::from(String::from_utf8_lossy(&output.stderr)),
    })
}

pub fn exec_target<I, S>(target_exe: &str, args: I) -> Result<OutputString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    exec_internal(target_exe, args, Vec::<(&str, &str)>::new(), None)
}

pub fn exec_target_with_env<I, S, IKV, K, V>(
    target_exe: &str,
    args: I,
    env: IKV,
) -> Result<OutputString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
    IKV: IntoIterator<Item = (K, V)>,
    K: AsRef<OsStr>,
    V: AsRef<OsStr>,
{
    exec_internal(target_exe, args, env, None)
}

pub fn exec_target_with_in<I, S>(target_exe: &str, args: I, in_bytes: &[u8]) -> Result<OutputString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    exec_internal(target_exe, args, Vec::<(&str, &str)>::new(), Some(in_bytes))
}

///
/// This invokes external a command and manipulates standard in out.
/// You can use `std::process::Command` more easily.
///
/// # Example
///
/// ```
/// use exec_target::exec_target_with_env_in;
///
/// let command = "target/debug/exe-stab-grep";
/// let args = &["--color=always", "-e", "c"];
/// let envs = vec![("GREP_COLORS", "ms=01;32")];
/// let inp = b"abcdefg\n" as &[u8];
///
/// let oup = exec_target_with_env_in(command, args, envs, inp).unwrap();
///
/// assert_eq!(oup.stderr, "");
/// assert_eq!(oup.stdout, "ab\u{1b}[01;32m\u{1b}[Kc\u{1b}[m\u{1b}[Kdefg\n");
/// assert_eq!(oup.status.success(), true);
/// ```
///
pub fn exec_target_with_env_in<I, S, IKV, K, V>(
    target_exe: &str,
    args: I,
    env: IKV,
    in_bytes: &[u8],
) -> Result<OutputString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
    IKV: IntoIterator<Item = (K, V)>,
    K: AsRef<OsStr>,
    V: AsRef<OsStr>,
{
    exec_internal(target_exe, args, env, Some(in_bytes))
}

///
/// parse a command line strings
///
/// This separates the string with blanks.
/// This considers special characters.
///
/// the special characters:
/// - "" : double quote
/// - '' : single quote
/// - \\ : back_slash
///
pub fn args_from(s: &str) -> Vec<String> {
    let mut v: Vec<String> = Vec::new();
    let mut ss = String::new();
    let mut enter_q: bool = false;
    let mut enter_qq: bool = false;
    let mut back_slash: bool = false;
    //
    for c in s.chars() {
        if back_slash {
            ss.push(c);
            back_slash = false;
            continue;
        }
        if c == '\\' {
            back_slash = true;
            continue;
        }
        if enter_q {
            if c == '\'' {
                v.push(ss.clone());
                ss.clear();
                enter_q = false;
            } else {
                ss.push(c);
            }
            continue;
        }
        if enter_qq {
            if c == '\"' {
                v.push(ss.clone());
                ss.clear();
                enter_qq = false;
            } else {
                ss.push(c);
            }
            continue;
        }
        match c {
            '\'' => {
                enter_q = true;
                continue;
            }
            '\"' => {
                enter_qq = true;
                continue;
            }
            ' ' => {
                if !ss.is_empty() {
                    v.push(ss.clone());
                    ss.clear();
                }
            }
            _ => {
                ss.push(c);
            }
        }
    }
    if !ss.is_empty() {
        v.push(ss.clone());
        ss.clear();
    }
    //
    v
}