better_commands/
lib.rs

1#![doc = include_str!("../README.md")]
2use std::cmp::Ordering;
3use std::io::{BufRead, BufReader, Lines};
4use std::process::{ChildStderr, ChildStdout, Command, Stdio};
5use std::thread;
6use std::time::{Duration, Instant};
7
8mod tests;
9
10/// Holds the output for a command
11///
12/// Features the lines printed (see [`Line`]), the status code, the start time, end time, and duration
13///
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct CmdOutput {
16    lines: Option<Vec<Line>>,
17    status_code: Option<i32>,
18    start_time: Instant,
19    end_time: Instant,
20    duration: Duration,
21}
22
23impl CmdOutput {
24    /// Returns only lines printed to stdout
25    ///
26    /// <small>This is an [`Option`] because [`run_funcs`] cannot provide `lines`</small>
27    pub fn stdout(self) -> Option<Vec<Line>> {
28        self.lines.and_then(|lines| {
29            Some(
30                lines
31                    .into_iter()
32                    .filter(|line| line.printed_to == LineType::Stdout)
33                    .collect(),
34            )
35        })
36    }
37
38    /// Returns only lines printed to stderr
39    ///
40    /// <small>This is an [`Option`] because [`run_funcs`] cannot provide `lines`</small>
41    pub fn stderr(self) -> Option<Vec<Line>> {
42        self.lines.and_then(|lines| {
43            Some(
44                lines
45                    .into_iter()
46                    .filter(|line| line.printed_to == LineType::Stderr)
47                    .collect(),
48            )
49        })
50    }
51
52    /// Returns all lines printed by the [`Command`]\
53    /// Note: All functions are *guaranteed* to return either `Some()` or `None`, not either
54    ///
55    /// <small>This is an [`Option`] because [`run_funcs`] cannot provide `lines`</small>
56    pub fn lines(self) -> Option<Vec<Line>> {
57        return self.lines;
58    }
59
60    /// Returns the exit status code, if there was one
61    ///
62    /// Note that if the program exited due to a signal, like SIGKILL, it's possible it didn't exit with a status code, hence this being an [`Option`].
63    pub fn status_code(self) -> Option<i32> {
64        return self.status_code;
65    }
66
67    /// Returns the duration the command ran for
68    pub fn duration(self) -> Duration {
69        return self.duration;
70    }
71
72    /// Returns the time the command was started at
73    pub fn start_time(self) -> Instant {
74        return self.start_time;
75    }
76
77    /// Returns the time the command finished at
78    pub fn end_time(self) -> Instant {
79        return self.end_time;
80    }
81}
82
83/// Specifies what a line was printed to - stdout or stderr
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
85pub enum LineType {
86    Stdout,
87    Stderr,
88}
89
90/// A single line from the output of a command
91#[derive(Debug, Clone, PartialEq, Eq, Ord)]
92pub struct Line {
93    /// Which stream the line was printed to
94    pub printed_to: LineType,
95    /// When the line was printed
96    pub time: Instant,
97    /// The content printed to the line
98    pub content: String,
99}
100
101impl Line {
102    /// Creates a [`Line`] from a string printed to stdout
103    pub fn from_stdout<S: AsRef<str>>(content: S) -> Self {
104        return Line {
105            content: content.as_ref().to_string(),
106            printed_to: LineType::Stdout,
107            time: Instant::now(),
108        };
109    }
110
111    /// Creates a [`Line`] from a string printed to stderr
112    pub fn from_stderr<S: AsRef<str>>(content: S) -> Self {
113        return Line {
114            content: content.as_ref().to_string(),
115            printed_to: LineType::Stderr,
116            time: Instant::now(),
117        };
118    }
119}
120
121impl PartialOrd for Line {
122    fn ge(&self, other: &Line) -> bool {
123        if self.time >= other.time {
124            return true;
125        }
126        return false;
127    }
128
129    fn gt(&self, other: &Self) -> bool {
130        if self.time > other.time {
131            return true;
132        }
133        return false;
134    }
135
136    fn le(&self, other: &Self) -> bool {
137        if self.time <= other.time {
138            return true;
139        }
140        return false;
141    }
142
143    fn lt(&self, other: &Self) -> bool {
144        if self.time < other.time {
145            return true;
146        }
147        return false;
148    }
149
150    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
151        if self < other {
152            return Some(Ordering::Less);
153        }
154        if self > other {
155            return Some(Ordering::Greater);
156        }
157        return Some(Ordering::Equal);
158    }
159}
160
161/// Runs a command, returning a [`CmdOutput`] (which *will* contain `Some(lines)`, not a None)
162///
163/// Example:
164///
165/// ```
166/// use better_commands::run;
167/// use std::process::Command;
168/// let cmd = run(&mut Command::new("echo").arg("hi"));
169///
170/// // prints the following: [Line { printed_to: Stdout, time: Instant { tv_sec: 16316, tv_nsec: 283884648 }, content: "hi" }]
171/// // (timestamp varies)
172/// assert_eq!("hi", cmd.lines().unwrap()[0].content);
173/// ```
174pub fn run(command: &mut Command) -> CmdOutput {
175    // https://stackoverflow.com/a/72831067/16432246
176    let start = Instant::now();
177    let mut child = command
178        .stdout(Stdio::piped())
179        .stderr(Stdio::piped())
180        .spawn()
181        .unwrap();
182
183    let child_stdout = child.stdout.take().unwrap();
184    let child_stderr = child.stderr.take().unwrap();
185
186    let stdout_lines = BufReader::new(child_stdout).lines();
187    let stdout_thread = thread::spawn(move || {
188        let mut lines: Vec<Line> = Vec::new();
189        for line in stdout_lines {
190            lines.push(Line {
191                content: line.unwrap(),
192                printed_to: LineType::Stdout,
193                time: Instant::now(),
194            });
195        }
196        return lines;
197    });
198
199    let stderr_lines = BufReader::new(child_stderr).lines();
200    let stderr_thread = thread::spawn(move || {
201        let mut lines: Vec<Line> = Vec::new();
202        for line in stderr_lines {
203            let time = Instant::now();
204            lines.push(Line {
205                content: line.unwrap(),
206                printed_to: LineType::Stderr,
207                time: time,
208            });
209        }
210        return lines;
211    });
212
213    let status = child.wait().unwrap().code();
214    let end = Instant::now();
215
216    let mut lines = stdout_thread.join().unwrap();
217    lines.append(&mut stderr_thread.join().unwrap());
218    lines.sort();
219
220    return CmdOutput {
221        lines: Some(lines),
222        status_code: status,
223        start_time: start,
224        end_time: end,
225        duration: end.duration_since(start),
226    };
227}
228
229/// Runs a command while simultaneously running a provided [`Fn`] as the command prints line-by-line
230///
231/// The [`CmdOutput`] *will* be None; this does *not* handle the lines - if you need them, use [`run`] or [`run_funcs_with_lines`]
232///
233/// Example:
234///
235/// ```
236/// use better_commands::run_funcs;
237/// use better_commands::Line;
238/// use std::process::Command;
239/// run_funcs(&mut Command::new("echo").arg("hi"), {
240///     |stdout_lines| {
241///         for line in stdout_lines {
242///             /* send line to database */
243///             }
244///         }
245///     },
246///     {
247///     |stderr_lines| {
248///         // this code is for stderr and won't run because echo won't print anything to stderr
249///     }
250/// });
251/// ```
252pub fn run_funcs(
253    command: &mut Command,
254    stdout_func: impl FnOnce(Lines<BufReader<ChildStdout>>) -> () + std::marker::Send + 'static,
255    stderr_func: impl FnOnce(Lines<BufReader<ChildStderr>>) -> () + std::marker::Send + 'static,
256) -> CmdOutput {
257    // https://stackoverflow.com/a/72831067/16432246
258    let start = Instant::now();
259    let mut child = command
260        .stdout(Stdio::piped())
261        .stderr(Stdio::piped())
262        .spawn()
263        .unwrap();
264
265    let child_stdout = child.stdout.take().unwrap();
266    let child_stderr = child.stderr.take().unwrap();
267
268    let stdout_lines = BufReader::new(child_stdout).lines();
269    let stdout_thread = thread::spawn(move || stdout_func(stdout_lines));
270
271    let stderr_lines = BufReader::new(child_stderr).lines();
272    let stderr_thread = thread::spawn(move || stderr_func(stderr_lines));
273
274    let status = child.wait().unwrap().code();
275    let end = Instant::now();
276
277    stdout_thread.join().unwrap();
278    stderr_thread.join().unwrap();
279
280    return CmdOutput {
281        lines: None,
282        status_code: status,
283        start_time: start,
284        end_time: end,
285        duration: end.duration_since(start),
286    };
287}
288
289/// Runs a command while simultaneously running a provided [`Fn`] as the command prints line-by-line, including line handling
290///
291/// The [`CmdOutput`] *will* contain `Some(lines)`, not a None.
292///
293/// Example:
294///
295/// ```
296/// use better_commands::run_funcs_with_lines;
297/// use better_commands::Line;
298/// use std::process::Command;
299/// let cmd = run_funcs_with_lines(&mut Command::new("echo").arg("hi"), {
300///     |stdout_lines| { // your function *must* return the lines
301///         let mut lines = Vec::new();
302///         for line in stdout_lines {
303///             lines.push(Line::from_stdout(line.unwrap()));
304///             /* send line to database */
305///             }
306///             return lines;
307///         }
308///     },
309///     {
310///     |stderr_lines| {
311///         // this code is for stderr and won't run because echo won't print anything to stderr, so we'll just put this placeholder here
312///         return Vec::new();
313///     }
314/// });
315///
316/// // prints the following: [Line { printed_to: Stdout, time: Instant { tv_sec: 16316, tv_nsec: 283884648 }, content: "hi" }]
317/// // (timestamp varies)
318/// assert_eq!("hi", cmd.lines().unwrap()[0].content);
319/// ```
320///
321/// In order for the built-in `lines` functionality to work, your function must return the lines like this; if this doesn't work for you, you can use [`run`] or [`run_funcs`] instead.
322/// ```ignore
323/// use better_commands::Line;
324///
325/// let mut lines = Vec::new();
326/// for line in stdout_lines {
327///     lines.push(Line::from_stdout(line.unwrap())); // from_stdout/from_stderr depending on which
328/// }
329/// return lines;
330/// ```
331pub fn run_funcs_with_lines(
332    command: &mut Command,
333    stdout_func: impl FnOnce(Lines<BufReader<ChildStdout>>) -> Vec<Line> + std::marker::Send + 'static,
334    stderr_func: impl FnOnce(Lines<BufReader<ChildStderr>>) -> Vec<Line> + std::marker::Send + 'static,
335) -> CmdOutput {
336    // https://stackoverflow.com/a/72831067/16432246
337    let start = Instant::now();
338    let mut child = command
339        .stdout(Stdio::piped())
340        .stderr(Stdio::piped())
341        .spawn()
342        .unwrap();
343
344    let child_stdout = child.stdout.take().unwrap();
345    let child_stderr = child.stderr.take().unwrap();
346
347    let stdout_lines = BufReader::new(child_stdout).lines();
348    let stderr_lines = BufReader::new(child_stderr).lines();
349
350    let stdout_thread = thread::spawn(move || stdout_func(stdout_lines));
351    let stderr_thread = thread::spawn(move || stderr_func(stderr_lines));
352
353    let mut lines = stdout_thread.join().unwrap();
354    let mut lines_printed_to_stderr = stderr_thread.join().unwrap();
355    lines.append(&mut lines_printed_to_stderr);
356    lines.sort();
357
358    let status = child.wait().unwrap().code();
359    let end = Instant::now();
360
361    return CmdOutput {
362        lines: Some(lines),
363        status_code: status,
364        start_time: start,
365        end_time: end,
366        duration: end.duration_since(start),
367    };
368}