Skip to main content

exec_target/
lib.rs

1/*!
2the simple invoke command for test
3
4This invokes external a command and manipulates standard in out.
5You can use `std::process::Command` more easily.
6
7# Features
8
9- minimum support rustc 1.58.0 (02072b482 2022-01-11)
10
11# Reproducible Tests and Environment
12
13To ensure reproducible test results across different environments, `exec-target` is opinionated about environment variables.
14By default, it clears the environment and only inherits a minimal set of essential variables:
15- `TERM`
16- `TZ`
17- `PATH`
18- `LD_LIBRARY_PATH`
19
20Additionally, 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.
21
22# Example
23
24```rust
25use exec_target::exec_target_with_env_in;
26
27let command = "target/debug/exe-stab-grep";
28let args = &["--color=always", "-e", "c"];
29let envs = vec![("GREP_COLORS", "ms=01;32")];
30let inp = b"abcdefg\n" as &[u8];
31
32let oup = exec_target_with_env_in(command, args, envs, inp).unwrap();
33
34assert_eq!(oup.stderr, "");
35assert_eq!(oup.stdout, "ab\u{1b}[01;32m\u{1b}[Kc\u{1b}[m\u{1b}[Kdefg\n");
36assert_eq!(oup.status.success(), true);
37```
38*/
39use std::collections::HashMap;
40use std::env;
41use std::ffi::OsStr;
42use std::process::{Command, ExitStatus, Output, Stdio};
43
44// trats
45use std::io::Write;
46use std::iter::IntoIterator;
47
48//
49/// Captured output from an executed command.
50///
51/// Both standard output and standard error are captured and converted
52/// to `String` using [`String::from_utf8_lossy`].
53pub struct OutputString {
54    /// The exit status of the command.
55    pub status: ExitStatus,
56    /// The captured standard output (stdout) as a string.
57    pub stdout: String,
58    /// The captured standard error (stderr) as a string.
59    pub stderr: String,
60}
61
62/// Error type for command execution.
63#[derive(Debug)]
64pub enum ExecError {
65    /// A general I/O error occurred during the process execution or piping.
66    Io(std::io::Error),
67    /// Failed to spawn the command. Includes the name of the executable.
68    SpawnFailed(String, std::io::Error),
69}
70
71impl std::fmt::Display for ExecError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            ExecError::Io(err) => write!(f, "IO error: {}", err),
75            ExecError::SpawnFailed(cmd, err) => write!(f, "failed to spawn {}: {}", cmd, err),
76        }
77    }
78}
79
80impl std::error::Error for ExecError {}
81
82impl From<std::io::Error> for ExecError {
83    fn from(err: std::io::Error) -> Self {
84        ExecError::Io(err)
85    }
86}
87
88pub type Result<T> = std::result::Result<T, ExecError>;
89
90fn setup_envs<I, K, V>(cmd: &mut Command, vars: I) -> &mut Command
91where
92    I: IntoIterator<Item = (K, V)>,
93    K: AsRef<OsStr>,
94    V: AsRef<OsStr>,
95{
96    let filtered_env: HashMap<String, String> = env::vars()
97        .filter(|(k, _)| k == "TERM" || k == "TZ" || k == "PATH" || k == "LD_LIBRARY_PATH")
98        .collect();
99    cmd.env_clear()
100        .envs(filtered_env)
101        .envs(vars)
102        .env("LANG", "C")
103}
104
105fn exec_internal<I, S, IKV, K, V>(
106    target_exe: &str,
107    args: I,
108    env: IKV,
109    in_bytes: Option<&[u8]>,
110) -> Result<OutputString>
111where
112    I: IntoIterator<Item = S>,
113    S: AsRef<OsStr>,
114    IKV: IntoIterator<Item = (K, V)>,
115    K: AsRef<OsStr>,
116    V: AsRef<OsStr>,
117{
118    let mut cmd: Command = Command::new(target_exe);
119    setup_envs(&mut cmd, env).args(args);
120
121    if in_bytes.is_some() {
122        cmd.stdin(Stdio::piped());
123    }
124    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
125
126    let mut child = cmd
127        .spawn()
128        .map_err(|e| ExecError::SpawnFailed(target_exe.to_string(), e))?;
129
130    if let Some(bytes) = in_bytes {
131        let stdin = child
132            .stdin
133            .as_mut()
134            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "failed to get stdin"))?;
135        let r = stdin.write_all(bytes);
136        match r {
137            Err(ioe) if ioe.kind() == std::io::ErrorKind::BrokenPipe => {
138                // nothing todo
139            }
140            _ => {
141                r?;
142            }
143        }
144    }
145
146    let output: Output = child.wait_with_output()?;
147    //
148    Ok(OutputString {
149        status: output.status,
150        stdout: String::from(String::from_utf8_lossy(&output.stdout)),
151        stderr: String::from(String::from_utf8_lossy(&output.stderr)),
152    })
153}
154
155pub fn exec_target<I, S>(target_exe: &str, args: I) -> Result<OutputString>
156where
157    I: IntoIterator<Item = S>,
158    S: AsRef<OsStr>,
159{
160    exec_internal(target_exe, args, Vec::<(&str, &str)>::new(), None)
161}
162
163pub fn exec_target_with_env<I, S, IKV, K, V>(
164    target_exe: &str,
165    args: I,
166    env: IKV,
167) -> Result<OutputString>
168where
169    I: IntoIterator<Item = S>,
170    S: AsRef<OsStr>,
171    IKV: IntoIterator<Item = (K, V)>,
172    K: AsRef<OsStr>,
173    V: AsRef<OsStr>,
174{
175    exec_internal(target_exe, args, env, None)
176}
177
178pub fn exec_target_with_in<I, S>(target_exe: &str, args: I, in_bytes: &[u8]) -> Result<OutputString>
179where
180    I: IntoIterator<Item = S>,
181    S: AsRef<OsStr>,
182{
183    exec_internal(target_exe, args, Vec::<(&str, &str)>::new(), Some(in_bytes))
184}
185
186///
187/// This invokes external a command and manipulates standard in out.
188/// You can use `std::process::Command` more easily.
189///
190/// # Example
191///
192/// ```
193/// use exec_target::exec_target_with_env_in;
194///
195/// let command = "target/debug/exe-stab-grep";
196/// let args = &["--color=always", "-e", "c"];
197/// let envs = vec![("GREP_COLORS", "ms=01;32")];
198/// let inp = b"abcdefg\n" as &[u8];
199///
200/// let oup = exec_target_with_env_in(command, args, envs, inp).unwrap();
201///
202/// assert_eq!(oup.stderr, "");
203/// assert_eq!(oup.stdout, "ab\u{1b}[01;32m\u{1b}[Kc\u{1b}[m\u{1b}[Kdefg\n");
204/// assert_eq!(oup.status.success(), true);
205/// ```
206///
207pub fn exec_target_with_env_in<I, S, IKV, K, V>(
208    target_exe: &str,
209    args: I,
210    env: IKV,
211    in_bytes: &[u8],
212) -> Result<OutputString>
213where
214    I: IntoIterator<Item = S>,
215    S: AsRef<OsStr>,
216    IKV: IntoIterator<Item = (K, V)>,
217    K: AsRef<OsStr>,
218    V: AsRef<OsStr>,
219{
220    exec_internal(target_exe, args, env, Some(in_bytes))
221}
222
223///
224/// parse a command line strings
225///
226/// This separates the string with blanks.
227/// This considers special characters.
228///
229/// the special characters:
230/// - "" : double quote
231/// - '' : single quote
232/// - \\ : back_slash
233///
234pub fn args_from(s: &str) -> Vec<String> {
235    let mut v: Vec<String> = Vec::new();
236    let mut ss = String::new();
237    let mut enter_q: bool = false;
238    let mut enter_qq: bool = false;
239    let mut back_slash: bool = false;
240    //
241    for c in s.chars() {
242        if back_slash {
243            ss.push(c);
244            back_slash = false;
245            continue;
246        }
247        if c == '\\' {
248            back_slash = true;
249            continue;
250        }
251        if enter_q {
252            if c == '\'' {
253                v.push(ss.clone());
254                ss.clear();
255                enter_q = false;
256            } else {
257                ss.push(c);
258            }
259            continue;
260        }
261        if enter_qq {
262            if c == '\"' {
263                v.push(ss.clone());
264                ss.clear();
265                enter_qq = false;
266            } else {
267                ss.push(c);
268            }
269            continue;
270        }
271        match c {
272            '\'' => {
273                enter_q = true;
274                continue;
275            }
276            '\"' => {
277                enter_qq = true;
278                continue;
279            }
280            ' ' => {
281                if !ss.is_empty() {
282                    v.push(ss.clone());
283                    ss.clear();
284                }
285            }
286            _ => {
287                ss.push(c);
288            }
289        }
290    }
291    if !ss.is_empty() {
292        v.push(ss.clone());
293        ss.clear();
294    }
295    //
296    v
297}