command_ext/log/
mod.rs

1//! Extension trait to log properties of a command
2//!
3//! # Example
4//!
5//! ```rust
6//! # use std::process::Command;
7//! # use command_ext::{CommandExtLog, CommandWrap};
8//! # use log::Level;
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! let output = Command::new("echo")
11//!     .arg("x")
12//!     .log_args(Level::Debug)
13//!     .log_status(Level::Info)
14//!     .log_stdout(Level::Trace)
15//!     .log_stderr(Level::Warn)
16//!     .output()?;
17//! # Ok(())
18//! # }
19//! ```
20
21use log::{log, Level};
22use std::{ffi::OsStr, process::Command};
23use typed_builder::TypedBuilder;
24
25use crate::{wrap::HasCommand, CommandWrap};
26#[cfg(feature = "check")]
27use crate::{CommandExtCheck, CommandExtError};
28
29#[derive(TypedBuilder, Debug)]
30pub struct CommandLog<'a> {
31    command: &'a mut Command,
32    #[builder(default, setter(into, strip_option))]
33    /// The log level for args before execution
34    args: Option<Level>,
35    #[builder(default, setter(into, strip_option))]
36    /// Whether to log the environment on execution
37    envs: Option<Level>,
38    #[builder(default, setter(into, strip_option))]
39    /// Whether to log the current directory on execution
40    current_dir: Option<Level>,
41    #[builder(default, setter(into, strip_option))]
42    /// Whether to log the status after execution
43    status: Option<Level>,
44    #[builder(default, setter(into, strip_option))]
45    /// Whether to log stdout after execution
46    stdout: Option<Level>,
47    #[builder(default, setter(into, strip_option))]
48    /// Whether to log stderr after execution
49    stderr: Option<Level>,
50}
51
52impl<'a> CommandLog<'a> {
53    fn log_before(&mut self) {
54        if let Some(args) = self.args {
55            log!(
56                args,
57                "args: {} {}",
58                self.command().get_program().to_string_lossy(),
59                self.command()
60                    .get_args()
61                    .collect::<Vec<_>>()
62                    .join(OsStr::new(" "))
63                    .to_string_lossy()
64            );
65        }
66
67        if let Some(envs) = self.envs {
68            self.command().get_envs().for_each(|(k, v)| {
69                log!(
70                    envs,
71                    "envs: {}={}",
72                    k.to_string_lossy(),
73                    v.unwrap_or_default().to_string_lossy()
74                );
75            });
76        }
77
78        if let Some(current_dir) = self.current_dir {
79            log!(
80                current_dir,
81                "current_dir: {}",
82                self.command()
83                    .get_current_dir()
84                    .map(|d| d.to_string_lossy())
85                    .unwrap_or_default()
86            );
87        }
88    }
89}
90
91impl<'a> HasCommand for CommandLog<'a> {
92    fn command(&self) -> &Command {
93        self.command
94    }
95
96    fn command_mut(&mut self) -> &mut Command {
97        self.command
98    }
99}
100
101impl<'a> CommandWrap for CommandLog<'a> {
102    fn on_spawn(&mut self) {
103        self.log_before();
104    }
105
106    fn on_output(&mut self) {
107        self.log_before();
108    }
109
110    fn on_status(&mut self) {
111        self.log_before();
112    }
113
114    fn after_output(&mut self, output: &std::io::Result<std::process::Output>) {
115        if let Ok(output) = output {
116            if let Some(status) = self.status {
117                log!(status, "status: {}", output.status);
118            }
119            if let Some(stdout) = self.stdout {
120                let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
121                if !out.is_empty() {
122                    log!(stdout, "stdout: {out}",);
123                }
124            }
125            if let Some(stderr) = self.stderr {
126                let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
127                if !err.is_empty() {
128                    log!(stderr, "stderr: {err}",);
129                }
130            }
131        }
132    }
133
134    fn after_status(&mut self, status: &std::io::Result<std::process::ExitStatus>) {
135        if let Ok(status) = status {
136            if let Some(status_filter) = self.status {
137                log!(status_filter, "status: {}", status);
138            }
139        }
140    }
141}
142
143impl<'a> From<&'a mut Command> for CommandLog<'a> {
144    fn from(value: &'a mut Command) -> Self {
145        Self::builder().command(value).build()
146    }
147}
148
149pub trait CommandExtLog {
150    fn log_args<L>(&mut self, filter: L) -> CommandLog
151    where
152        L: Into<Level>;
153    fn log_envs<L>(&mut self, filter: L) -> CommandLog
154    where
155        L: Into<Level>;
156    fn log_current_dir<L>(&mut self, filter: L) -> CommandLog
157    where
158        L: Into<Level>;
159    fn log_status<L>(&mut self, filter: L) -> CommandLog
160    where
161        L: Into<Level>;
162    fn log_stdout<L>(&mut self, filter: L) -> CommandLog
163    where
164        L: Into<Level>;
165    fn log_stderr<L>(&mut self, filter: L) -> CommandLog
166    where
167        L: Into<Level>;
168}
169
170impl CommandExtLog for Command {
171    fn log_args<L>(&mut self, filter: L) -> CommandLog
172    where
173        L: Into<Level>,
174    {
175        CommandLog::builder().command(self).args(filter).build()
176    }
177
178    fn log_envs<L>(&mut self, filter: L) -> CommandLog
179    where
180        L: Into<Level>,
181    {
182        CommandLog::builder().command(self).envs(filter).build()
183    }
184
185    fn log_current_dir<L>(&mut self, filter: L) -> CommandLog
186    where
187        L: Into<Level>,
188    {
189        CommandLog::builder()
190            .command(self)
191            .current_dir(filter)
192            .build()
193    }
194
195    fn log_status<L>(&mut self, filter: L) -> CommandLog
196    where
197        L: Into<Level>,
198    {
199        CommandLog::builder().command(self).status(filter).build()
200    }
201
202    fn log_stdout<L>(&mut self, filter: L) -> CommandLog
203    where
204        L: Into<Level>,
205    {
206        CommandLog::builder().command(self).stdout(filter).build()
207    }
208
209    fn log_stderr<L>(&mut self, filter: L) -> CommandLog
210    where
211        L: Into<Level>,
212    {
213        CommandLog::builder().command(self).stderr(filter).build()
214    }
215}
216
217impl<'a> CommandLog<'a> {
218    pub fn log_args<L>(&'a mut self, filter: L) -> &'a mut CommandLog
219    where
220        L: Into<Level>,
221    {
222        self.args = Some(filter.into());
223        self
224    }
225
226    pub fn log_envs<L>(&'a mut self, filter: L) -> &'a mut CommandLog
227    where
228        L: Into<Level>,
229    {
230        self.envs = Some(filter.into());
231        self
232    }
233
234    pub fn log_current_dir<L>(&'a mut self, filter: L) -> &'a mut CommandLog
235    where
236        L: Into<Level>,
237    {
238        self.current_dir = Some(filter.into());
239        self
240    }
241
242    pub fn log_status<L>(&'a mut self, filter: L) -> &'a mut CommandLog
243    where
244        L: Into<Level>,
245    {
246        self.status = Some(filter.into());
247        self
248    }
249
250    pub fn log_stdout<L>(&'a mut self, filter: L) -> &'a mut CommandLog
251    where
252        L: Into<Level>,
253    {
254        self.stdout = Some(filter.into());
255        self
256    }
257
258    pub fn log_stderr<L>(&'a mut self, filter: L) -> &'a mut CommandLog
259    where
260        L: Into<Level>,
261    {
262        self.stderr = Some(filter.into());
263        self
264    }
265}
266
267#[cfg(feature = "check")]
268impl<'a> CommandExtCheck for CommandLog<'a> {
269    type Error = CommandExtError;
270
271    fn check(&mut self) -> Result<std::process::Output, Self::Error> {
272        self.output().map_err(CommandExtError::from).and_then(|r| {
273            r.status
274                .success()
275                .then_some(r.clone())
276                .ok_or_else(|| CommandExtError::Check {
277                    status: r.status,
278                    stdout: String::from_utf8_lossy(&r.stdout).to_string(),
279                    stderr: String::from_utf8_lossy(&r.stderr).to_string(),
280                })
281        })
282    }
283}
284
285#[cfg(test)]
286mod test {
287    use log::Level;
288    use std::process::Command;
289    use test_log::test;
290
291    use crate::{CommandExtLog, CommandWrap};
292
293    #[test]
294    #[cfg_attr(miri, ignore)]
295    fn test_args() -> anyhow::Result<()> {
296        Command::new("echo")
297            .arg("x")
298            .log_args(Level::Error)
299            .output()?;
300        Ok(())
301    }
302
303    #[test]
304    #[cfg_attr(miri, ignore)]
305    fn test_envs() -> anyhow::Result<()> {
306        Command::new("echo")
307            .env("x", "y")
308            .log_envs(Level::Error)
309            .output()?;
310        Ok(())
311    }
312
313    #[test]
314    #[cfg_attr(miri, ignore)]
315    fn test_current_dir() -> anyhow::Result<()> {
316        Command::new("echo")
317            .current_dir(env!("CARGO_MANIFEST_DIR"))
318            .log_current_dir(Level::Error)
319            .output()?;
320        Ok(())
321    }
322
323    #[test]
324    #[cfg_attr(miri, ignore)]
325    fn test_status() -> anyhow::Result<()> {
326        Command::new("echo")
327            .arg("x")
328            .log_status(Level::Error)
329            .output()?;
330
331        Ok(())
332    }
333
334    #[test]
335    #[cfg_attr(miri, ignore)]
336    fn test_stdout() -> anyhow::Result<()> {
337        Command::new("echo")
338            .arg("x")
339            .log_stdout(Level::Error)
340            .output()?;
341
342        Ok(())
343    }
344
345    #[test]
346    #[cfg_attr(miri, ignore)]
347    fn test_stderr() -> anyhow::Result<()> {
348        Command::new("bash")
349            .args(["-c", "echo y 1>&2"])
350            .log_stderr(Level::Error)
351            .output()?;
352
353        Ok(())
354    }
355}