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