1use 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 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
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}