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