1use std::{ffi::OsStr, process::Command};
22use tracing::{debug, error, info, trace, warn, Level};
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 CommandTrace<'a> {
31 command: &'a mut Command,
32 #[builder(default, setter(into, strip_option))]
33 args: Option<Level>,
35 #[builder(default, setter(into, strip_option))]
36 envs: Option<Level>,
38 #[builder(default, setter(into, strip_option))]
39 current_dir: Option<Level>,
41 #[builder(default, setter(into, strip_option))]
42 status: Option<Level>,
44 #[builder(default, setter(into, strip_option))]
45 stdout: Option<Level>,
47 #[builder(default, setter(into, strip_option))]
48 stderr: Option<Level>,
50}
51
52macro_rules! log {
53 ($lvl:expr, $fmt:expr, $($arg:tt)*) => {
54 match $lvl {
55 Level::TRACE => {
56 trace!($fmt, $($arg)*);
57 }
58 Level::DEBUG => {
59 debug!($fmt, $($arg)*);
60 }
61 Level::INFO => {
62 info!($fmt, $($arg)*);
63 }
64 Level::WARN => {
65 warn!($fmt, $($arg)*);
66 }
67 Level::ERROR => {
68 error!($fmt, $($arg)*);
69 }
70 }
71 }
72}
73
74impl<'a> CommandTrace<'a> {
75 fn trace_before(&mut self) {
76 if let Some(args) = self.args {
77 log!(
78 args,
79 "args: {} {}",
80 self.command().get_program().to_string_lossy(),
81 self.command()
82 .get_args()
83 .collect::<Vec<_>>()
84 .join(OsStr::new(" "))
85 .to_string_lossy()
86 );
87 }
88
89 if let Some(envs) = self.envs {
90 self.command().get_envs().for_each(|(k, v)| {
91 log!(
92 envs,
93 "envs: {}={}",
94 k.to_string_lossy(),
95 v.unwrap_or_default().to_string_lossy()
96 );
97 });
98 }
99
100 if let Some(current_dir) = self.current_dir {
101 log!(
102 current_dir,
103 "current_dir: {}",
104 self.command()
105 .get_current_dir()
106 .map(|d| d.to_string_lossy())
107 .unwrap_or_default()
108 );
109 }
110 }
111}
112
113impl<'a> HasCommand for CommandTrace<'a> {
114 fn command(&self) -> &Command {
115 self.command
116 }
117
118 fn command_mut(&mut self) -> &mut Command {
119 self.command
120 }
121}
122
123impl<'a> CommandWrap for CommandTrace<'a> {
124 fn on_spawn(&mut self) {
125 self.trace_before();
126 }
127
128 fn on_output(&mut self) {
129 self.trace_before();
130 }
131
132 fn on_status(&mut self) {
133 self.trace_before();
134 }
135
136 fn after_output(&mut self, output: &std::io::Result<std::process::Output>) {
137 if let Ok(output) = output {
138 if let Some(status) = self.status {
139 log!(status, "status: {}", output.status);
140 }
141
142 if let Some(stdout) = self.stdout {
143 let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
144 if !out.is_empty() {
145 log!(stdout, "stdout: {out}",);
146 }
147 }
148 if let Some(stderr) = self.stderr {
149 let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
150 if !err.is_empty() {
151 log!(stderr, "stderr: {err}",);
152 }
153 }
154 }
155 }
156
157 fn after_status(&mut self, status: &std::io::Result<std::process::ExitStatus>) {
158 if let Ok(status) = status {
159 if let Some(status_filter) = self.status {
160 log!(status_filter, "status: {}", status);
161 }
162 }
163 }
164}
165
166impl<'a> From<&'a mut Command> for CommandTrace<'a> {
167 fn from(value: &'a mut Command) -> Self {
168 Self::builder().command(value).build()
169 }
170}
171
172pub trait CommandExtTrace {
173 fn trace_args<L>(&mut self, filter: L) -> CommandTrace
174 where
175 L: Into<Level>;
176 fn trace_envs<L>(&mut self, filter: L) -> CommandTrace
177 where
178 L: Into<Level>;
179 fn trace_current_dir<L>(&mut self, filter: L) -> CommandTrace
180 where
181 L: Into<Level>;
182 fn trace_status<L>(&mut self, filter: L) -> CommandTrace
183 where
184 L: Into<Level>;
185 fn trace_stdout<L>(&mut self, filter: L) -> CommandTrace
186 where
187 L: Into<Level>;
188 fn trace_stderr<L>(&mut self, filter: L) -> CommandTrace
189 where
190 L: Into<Level>;
191}
192
193impl CommandExtTrace for Command {
194 fn trace_args<L>(&mut self, filter: L) -> CommandTrace
195 where
196 L: Into<Level>,
197 {
198 CommandTrace::builder().command(self).args(filter).build()
199 }
200
201 fn trace_envs<L>(&mut self, filter: L) -> CommandTrace
202 where
203 L: Into<Level>,
204 {
205 CommandTrace::builder().command(self).envs(filter).build()
206 }
207
208 fn trace_current_dir<L>(&mut self, filter: L) -> CommandTrace
209 where
210 L: Into<Level>,
211 {
212 CommandTrace::builder()
213 .command(self)
214 .current_dir(filter)
215 .build()
216 }
217
218 fn trace_status<L>(&mut self, filter: L) -> CommandTrace
219 where
220 L: Into<Level>,
221 {
222 CommandTrace::builder().command(self).status(filter).build()
223 }
224
225 fn trace_stdout<L>(&mut self, filter: L) -> CommandTrace
226 where
227 L: Into<Level>,
228 {
229 CommandTrace::builder().command(self).stdout(filter).build()
230 }
231
232 fn trace_stderr<L>(&mut self, filter: L) -> CommandTrace
233 where
234 L: Into<Level>,
235 {
236 CommandTrace::builder().command(self).stderr(filter).build()
237 }
238}
239
240impl<'a> CommandTrace<'a> {
241 pub fn trace_args<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
242 where
243 L: Into<Level>,
244 {
245 self.args = Some(filter.into());
246 self
247 }
248
249 pub fn trace_envs<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
250 where
251 L: Into<Level>,
252 {
253 self.envs = Some(filter.into());
254 self
255 }
256
257 pub fn trace_current_dir<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
258 where
259 L: Into<Level>,
260 {
261 self.current_dir = Some(filter.into());
262 self
263 }
264
265 pub fn trace_status<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
266 where
267 L: Into<Level>,
268 {
269 self.status = Some(filter.into());
270 self
271 }
272
273 pub fn trace_stdout<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
274 where
275 L: Into<Level>,
276 {
277 self.stdout = Some(filter.into());
278 self
279 }
280
281 pub fn trace_stderr<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
282 where
283 L: Into<Level>,
284 {
285 self.stderr = Some(filter.into());
286 self
287 }
288}
289
290#[cfg(feature = "check")]
291impl<'a> CommandExtCheck for CommandTrace<'a> {
292 type Error = CommandExtError;
293
294 fn check(&mut self) -> Result<std::process::Output, Self::Error> {
295 self.output().map_err(CommandExtError::from).and_then(|r| {
296 r.status
297 .success()
298 .then_some(r.clone())
299 .ok_or_else(|| CommandExtError::Check {
300 status: r.status,
301 stdout: String::from_utf8_lossy(&r.stdout).to_string(),
302 stderr: String::from_utf8_lossy(&r.stderr).to_string(),
303 })
304 })
305 }
306}
307
308#[cfg(test)]
309mod test {
310 use std::process::Command;
311 use test_log::test;
312 use tracing::Level;
313
314 use crate::{CommandExtTrace, CommandWrap};
315
316 #[test]
317 #[cfg_attr(miri, ignore)]
318 fn test_args() -> anyhow::Result<()> {
319 Command::new("echo")
320 .arg("x")
321 .trace_args(Level::ERROR)
322 .output()?;
323 Ok(())
324 }
325
326 #[test]
327 #[cfg_attr(miri, ignore)]
328 fn test_envs() -> anyhow::Result<()> {
329 Command::new("echo")
330 .env("x", "y")
331 .trace_envs(Level::ERROR)
332 .output()?;
333 Ok(())
334 }
335
336 #[test]
337 #[cfg_attr(miri, ignore)]
338 fn test_current_dir() -> anyhow::Result<()> {
339 Command::new("echo")
340 .current_dir(env!("CARGO_MANIFEST_DIR"))
341 .trace_current_dir(Level::ERROR)
342 .output()?;
343 Ok(())
344 }
345
346 #[test]
347 #[cfg_attr(miri, ignore)]
348 fn test_status() -> anyhow::Result<()> {
349 Command::new("echo")
350 .arg("x")
351 .trace_status(Level::ERROR)
352 .output()?;
353
354 Ok(())
355 }
356
357 #[test]
358 #[cfg_attr(miri, ignore)]
359 fn test_stdout() -> anyhow::Result<()> {
360 Command::new("echo")
361 .arg("x")
362 .trace_stdout(Level::ERROR)
363 .output()?;
364
365 Ok(())
366 }
367
368 #[test]
369 #[cfg_attr(miri, ignore)]
370 fn test_stderr() -> anyhow::Result<()> {
371 Command::new("bash")
372 .args(["-c", "echo y 1>&2"])
373 .trace_stderr(Level::ERROR)
374 .output()?;
375
376 Ok(())
377 }
378
379 #[test]
380 #[cfg_attr(miri, ignore)]
381 fn test_multi() -> anyhow::Result<()> {
382 Command::new("bash")
383 .args(["-c", "echo y 1>&2; echo x;"])
384 .env("x", "y")
385 .trace_args(Level::ERROR)
386 .trace_status(Level::ERROR)
387 .trace_stdout(Level::ERROR)
388 .trace_stderr(Level::ERROR)
389 .output()?;
390 Ok(())
391 }
392}