Julio Biason
1 year ago
1 changed files with 146 additions and 0 deletions
@ -0,0 +1,146 @@ |
|||||||
|
+++ |
||||||
|
title = "Running a Command and Saving Its Output to File in Rust" |
||||||
|
date = 2023-09-01 |
||||||
|
|
||||||
|
[taxonomies] |
||||||
|
tags = ["random", "rust", "command", "log"] |
||||||
|
+++ |
||||||
|
|
||||||
|
I had an issue: I needed to run a command inside Rust, but I needed that all |
||||||
|
its output should go to a file, and I needed to check if there were certain |
||||||
|
phrases in it. |
||||||
|
|
||||||
|
<!-- more --> |
||||||
|
|
||||||
|
So, first step: Create a script that could "replicate" the output of a command, |
||||||
|
with the expected strings to be captured: |
||||||
|
|
||||||
|
```bash |
||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
for loop in {1..1000} |
||||||
|
do |
||||||
|
echo "Hello, I'm a script!" |
||||||
|
echo "I write stuff in the output." |
||||||
|
echo "Everything should go to a file." |
||||||
|
echo "But also, you need to capture warnings:" |
||||||
|
|
||||||
|
if (( $loop%7 == 0)); then |
||||||
|
echo "WARNING: This is a warning" |
||||||
|
echo " It continues if the line starts with spaces" |
||||||
|
echo " And keeps going till there are no more spaces-prefixes" |
||||||
|
fi |
||||||
|
|
||||||
|
if (( $loop%8 == 0)); then |
||||||
|
# ERR is just to make sure we find it easily in the logs |
||||||
|
echo "ERR: Sometimes, I also write in stderr!" >&2 |
||||||
|
echo "ERR: Just for funsies!" >&2 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "Like this." |
||||||
|
echo "Then you're good to go." |
||||||
|
echo "" |
||||||
|
done |
||||||
|
``` |
||||||
|
|
||||||
|
What this script does is to print a message over 1,000 times, and sometimes it |
||||||
|
will display a "WARNING" text -- which is the special output I need to capture -- |
||||||
|
and sometimes it will print things to stderr. |
||||||
|
|
||||||
|
For the code, what we need to do is: |
||||||
|
|
||||||
|
1. Spawn the command; |
||||||
|
2. Take the stdour (and stderr) from it. |
||||||
|
3. Spawn a thread that will keep listening to the output, doing the search, |
||||||
|
and writing everything to a file (our log). |
||||||
|
4. The thread returns the list of captured messages, which we can get back |
||||||
|
when we `.join()` it. |
||||||
|
5. Since I was expecting stderr to be smaller enough, I did the capturing of |
||||||
|
it after the thread completes (which would also close the file, so we can |
||||||
|
be sure that we can open it again without any issues). |
||||||
|
|
||||||
|
The first step is quite easy: Just use `std::process::Command` and use the |
||||||
|
`.spawn()` function to create the `Child` controller. |
||||||
|
|
||||||
|
For the second step, we use the `Child` structure and use `.take()` on both |
||||||
|
stdout and stderr. This will give us the file descriptor for both (think about |
||||||
|
them as `File`s). |
||||||
|
|
||||||
|
The third step is quite easy, actualy: `std::thread::spawn()` to create a |
||||||
|
thread, and just read the content from the file descriptors from step 2. In |
||||||
|
this, I used `BufReader`, which gives access to reading the content line by |
||||||
|
line, which is way easier than reading to a buffer and processing it. |
||||||
|
|
||||||
|
```rust |
||||||
|
use std::fs::{File, OpenOptions}; |
||||||
|
use std::io::{BufRead, BufReader, Read, Write}; |
||||||
|
use std::path::PathBuf; |
||||||
|
use std::process::Command; |
||||||
|
|
||||||
|
fn main() { |
||||||
|
// this requires always running with `cargo run` |
||||||
|
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
||||||
|
let the_script = base.join("src").join("the_script.sh"); |
||||||
|
|
||||||
|
let mut cmd = Command::new("bash") |
||||||
|
.arg(the_script) |
||||||
|
.stdout(std::process::Stdio::piped()) |
||||||
|
.stderr(std::process::Stdio::piped()) |
||||||
|
.spawn() |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
// capture both the stdout and stderr as File structs (actually FDs, but basically the same |
||||||
|
// thing) |
||||||
|
let stdout = cmd.stdout.take().unwrap(); |
||||||
|
let mut stderr = cmd.stderr.take().unwrap(); |
||||||
|
|
||||||
|
// spawn a thread to keep capturing and processing the stdout. |
||||||
|
let writer_pid = std::thread::spawn(move || { |
||||||
|
let reader = BufReader::new(stdout); |
||||||
|
let lines = reader.lines(); |
||||||
|
let mut log_file = File::create("script.log").unwrap(); |
||||||
|
let mut in_warning = false; |
||||||
|
let mut result = Vec::new(); |
||||||
|
|
||||||
|
for line in lines { |
||||||
|
let line = line.unwrap(); |
||||||
|
log_file.write(line.as_bytes()).unwrap(); |
||||||
|
log_file.write(b"\n").unwrap(); // 'cause lines() eat it |
||||||
|
|
||||||
|
if line.starts_with("WARNING:") { |
||||||
|
in_warning = true; |
||||||
|
} else if line.starts_with(" ") && in_warning { |
||||||
|
result.push(line); |
||||||
|
} else if in_warning { |
||||||
|
in_warning = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
result |
||||||
|
}); |
||||||
|
|
||||||
|
// run the command till it finishes |
||||||
|
cmd.wait().unwrap(); |
||||||
|
|
||||||
|
// ... and wait till the thread finishes processing the whole output. |
||||||
|
let warnings = writer_pid.join().unwrap(); |
||||||
|
|
||||||
|
// this is somewhat a hack: Instead of spawning a thread for stderr and trying to fight with |
||||||
|
// stdout for the lock to be able to write in the log file, we do this after the thread ends |
||||||
|
// (which closes the file) and then open it again and write the stderr in the end. We do this |
||||||
|
// 'cause we expect that the stderr is way smaller than stdout and can fit in memory without |
||||||
|
// any issues. |
||||||
|
let mut buffer = String::new(); |
||||||
|
stderr.read_to_string(&mut buffer).unwrap(); |
||||||
|
|
||||||
|
let mut file = OpenOptions::new().append(true).open("script.log").unwrap(); |
||||||
|
file.write(buffer.as_bytes()).unwrap(); |
||||||
|
|
||||||
|
// This is purely for diagnostic purposes. We could put the warnings in another file, or pass |
||||||
|
// it along to something else to process it. Here, we just display them. |
||||||
|
// Same for stderr: Since we already put them in the file, this is used just to make sure we |
||||||
|
// are capturing the errors without looking at the file. |
||||||
|
println!("Warnings:\n{:?}", warnings); |
||||||
|
println!("ERR:\n{:?}", buffer) |
||||||
|
} |
||||||
|
``` |
Loading…
Reference in new issue