From 1cc6903c04c9c3f510a724fc1d440b6097748a8b Mon Sep 17 00:00:00 2001 From: askiiart Date: Wed, 1 Jan 2025 20:54:10 -0600 Subject: [PATCH 1/2] make release compile with opt level 3 --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 8581b51..8ddd026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,6 @@ version = "0.1.1" edition = "2021" license = "GPL-3.0-only" keywords = ["command", "cmd"] + +[profile.release] +opt-level = 3 \ No newline at end of file From 1f312e57e503a4b62772beb811aecc7aa7b03733 Mon Sep 17 00:00:00 2001 From: askiiart Date: Wed, 1 Jan 2025 21:05:47 -0600 Subject: [PATCH 2/2] add run_with_funcs() --- src/lib.rs | 260 +++++++++++++++++++++++++++++++++------------------ src/tests.rs | 71 ++++++++++++-- 2 files changed, 233 insertions(+), 98 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 53a100e..d0313e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use std::io::{BufRead, BufReader}; -use std::process::{Command, Stdio}; +use std::io::{BufRead, BufReader, Lines}; +use std::process::{ChildStderr, ChildStdout, Command, Stdio}; use std::thread; use std::time::{Duration, Instant}; @@ -9,61 +9,71 @@ mod tests; /// Holds the output for a command #[derive(Debug, Clone, PartialEq, Eq)] pub struct CmdOutput { - lines: Vec, - status: Option, + lines: Option>, + status_code: Option, start_time: Instant, end_time: Instant, -} - -#[derive(Debug, Clone, PartialEq, Eq, Ord)] -pub struct Line { - pub stdout: bool, - pub time: Instant, - pub content: String, + duration: Duration, } impl CmdOutput { /// Returns only stdout - pub fn stdout(self) -> Vec { - return self - .lines - .into_iter() - .filter(|l| { - if l.stdout { - return true; - } - return false; - }) - .collect(); + pub fn stdout(self) -> Option> { + match self.lines { + Some(lines) => { + return Some( + lines + .into_iter() + .filter(|l| { + if l.printed_to == LineType::Stdout { + return true; + } + return false; + }) + .collect(), + ); + } + None => { + return None; + } + } } /// Returns only stdout - pub fn stderr(self) -> Vec { - return self - .lines - .into_iter() - .filter(|l| { - if !l.stdout { - return true; - } - return false; - }) - .collect(); + pub fn stderr(self) -> Option> { + match self.lines { + Some(lines) => { + return Some( + lines + .into_iter() + .filter(|l| { + if l.printed_to == LineType::Stderr { + return true; + } + return false; + }) + .collect(), + ); + } + None => { + return None; + } + } } /// Returns all output - pub fn lines(self) -> Vec { + pub fn lines(self) -> Option> { return self.lines; } /// Returns the exit status code, if there was one - pub fn status(self) -> Option { - return self.status; + pub fn status_code(self) -> Option { + return self.status_code; } /// Returns the duration the command ran for pub fn duration(self) -> Duration { - return self.end_time.duration_since(self.start_time); + return self.duration; } /// Returns the time the command was started at @@ -77,61 +87,21 @@ impl CmdOutput { } } -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(); +/// Specifies what a line was printed to - stdout or stderr +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum LineType { + Stdout, + Stderr, +} - let child_stdout = child.stdout.take().unwrap(); - let child_stderr = child.stderr.take().unwrap(); - - let (stdout_tx, stdout_rx) = std::sync::mpsc::channel(); - let (stderr_tx, stderr_rx) = std::sync::mpsc::channel(); - - let stdout_lines = BufReader::new(child_stdout).lines(); - thread::spawn(move || { - for line in stdout_lines { - stdout_tx - .send(Line { - content: line.unwrap(), - stdout: true, - time: Instant::now(), - }) - .unwrap(); - } - }); - - let stderr_lines = BufReader::new(child_stderr).lines(); - thread::spawn(move || { - for line in stderr_lines { - let time = Instant::now(); - stderr_tx - .send(Line { - content: line.unwrap(), - stdout: false, - time: time, - }) - .unwrap(); - } - }); - - let status = child.wait().unwrap().code(); - let end = Instant::now(); - - let mut lines = stdout_rx.into_iter().collect::>(); - lines.append(&mut stderr_rx.into_iter().collect::>()); - //lines.sort(); - - return CmdOutput { - lines: lines, - status: status, - start_time: start, - end_time: end, - }; +/// A single line from the output of a command +/// +/// This contains what the line was printed to (stdout/stderr), a timestamp, and the content of course. +#[derive(Debug, Clone, PartialEq, Eq, Ord)] +pub struct Line { + pub printed_to: LineType, + pub time: Instant, + pub content: String, } impl PartialOrd for Line { @@ -173,3 +143,111 @@ impl PartialOrd for Line { return Some(Ordering::Equal); } } + +/// Runs a command, returning a +/// +/// 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) +/// println!("{:?}", cmd.lines().unwrap()); +/// ``` +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_tx, stdout_rx) = std::sync::mpsc::channel(); + let (stderr_tx, stderr_rx) = std::sync::mpsc::channel(); + + let stdout_lines = BufReader::new(child_stdout).lines(); + thread::spawn(move || { + for line in stdout_lines { + stdout_tx + .send(Line { + content: line.unwrap(), + printed_to: LineType::Stdout, + time: Instant::now(), + }) + .unwrap(); + } + }); + + let stderr_lines = BufReader::new(child_stderr).lines(); + thread::spawn(move || { + for line in stderr_lines { + let time = Instant::now(); + stderr_tx + .send(Line { + content: line.unwrap(), + printed_to: LineType::Stderr, + time: time, + }) + .unwrap(); + } + }); + + let status = child.wait().unwrap().code(); + let end = Instant::now(); + + let mut lines = stdout_rx.into_iter().collect::>(); + lines.append(&mut stderr_rx.into_iter().collect::>()); + //lines.sort(); + + return CmdOutput { + lines: Some(lines), + status_code: status, + start_time: start, + end_time: end, + duration: end.duration_since(start), + }; +} + +pub fn run_with_funcs( + command: &mut Command, + stdout_func: impl Fn(Lines>) -> () + std::marker::Send + 'static, + stderr_func: impl Fn(Lines>) -> () + 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), + }; +} diff --git a/src/tests.rs b/src/tests.rs index ed19735..d91aa69 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,6 +1,11 @@ #[cfg(test)] use crate::*; -use std::hash::{BuildHasher, Hasher, RandomState}; +use std::{ + fs::remove_file, + hash::{BuildHasher, Hasher, RandomState}, +}; +use std::{fs::File, os::unix::fs::FileExt, thread::sleep}; +use std::process::Command; /// Tests what stdout prints #[test] @@ -14,6 +19,7 @@ fn stdout_content() { .arg("-n") .arg("helloooooooooo\nhiiiiiiiiiiiii")) .stdout() + .unwrap() .into_iter() .map(|line| { line.content }) .collect::>() @@ -24,20 +30,18 @@ fn stdout_content() { /// Tests what stderr prints #[test] fn stderr_content() { - let expected = "[\"helloooooooooo\", \"hiiiiiiiiiiiii\"]"; + let expected = vec!["helloooooooooo", "hiiiiiiiiiiiii"]; // `>&2` redirects to stderr assert_eq!( expected, - format!( - "{:?}", run(&mut Command::new("bash") .arg("-c") .arg("echo -n 'helloooooooooo\nhiiiiiiiiiiiii' >&2")) .stderr() + .unwrap() .into_iter() .map(|line| { line.content }) .collect::>() - ) ); } @@ -49,7 +53,7 @@ fn test_exit_code() { assert_eq!( expected, run(&mut Command::new("bash").arg("-c").arg("exit 10")) - .status() + .status_code() .unwrap() ); } @@ -57,7 +61,11 @@ fn test_exit_code() { /// Tests that the output is sorted by default #[test] fn test_output_is_sorted_sort_works() { - let cmd = run(&mut Command::new("bash").arg("-c").arg("echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi; sleep 0.01; echo hi")).stdout(); + let cmd = run(&mut Command::new("bash") + .arg("-c") + .arg("echo hi; echo hi; echo hi; echo hi; echo hi")) + .stdout() + .unwrap(); let mut sorted = cmd.clone(); // To avoid an accidental bogosort while sorted.is_sorted() { @@ -78,3 +86,52 @@ fn shuffle_vec(vec: &mut [T]) { vec.swap(i, j); } } + +#[test] +fn test_run_with_funcs() { + let _ = thread::spawn(|| { + let _ = run_with_funcs( + Command::new("bash") + .arg("-c") + .arg("echo hi; sleep 0.5; >&2 echo hello"), + { + |stdout_lines| { + sleep(Duration::from_secs(1)); + for _ in stdout_lines { + Command::new("bash") + .arg("-c") + .arg("echo stdout >> ./tmp") + .output() + .unwrap(); + } + } + }, + { + |stderr_lines| { + sleep(Duration::from_secs(3)); + for _ in stderr_lines { + Command::new("bash") + .arg("-c") + .arg("echo stderr >> ./tmp") + .output() + .unwrap(); + } + } + }, + ); + }); + sleep(Duration::from_secs(2)); + let f = File::open("./tmp").unwrap(); + let mut buf: [u8; 14] = [0u8; 14]; + f.read_at(&mut buf, 0).unwrap(); + assert_eq!(buf, [115, 116, 100, 111, 117, 116, 10, 0, 0, 0, 0, 0, 0, 0]); + + sleep(Duration::from_secs(2)); + f.read_at(&mut buf, 0).unwrap(); + assert_eq!( + buf, + [115, 116, 100, 111, 117, 116, 10, 115, 116, 100, 101, 114, 114, 10] + ); + + remove_file("./tmp").unwrap(); +}