diff options
author | Rory Dudley | 2024-05-19 18:50:06 -0600 |
---|---|---|
committer | Rory Dudley | 2024-05-19 18:50:06 -0600 |
commit | 4b1b8061e79b42128df4f06fd1e439549bf9696b (patch) | |
tree | 80db43cf7295937751d61435fb4e60118b8a3ea9 /src/poem/elements/verse.rs | |
parent | 8756d3e7512c1416cc15a688c62b8f51f030b192 (diff) | |
download | dwarvish-4b1b8061e79b42128df4f06fd1e439549bf9696b.tar.gz |
Handle STDERR, in addition to STDOUT
This patch overhauls the reading and reciting of verses, such that the
redirection of STDERR (in addition to STDOUT, which was already a
feature), is now possible.
Removed the 'stdout' argument from recite(), since it is no longer
needed with how incantations function.
A verse's couplet indicator is now a u8, instead of a bool, with certain
values corresponding to types of couplets, for instance:
ls | grep Ca | lolcat
^ ^ ^
| | 2: right side of a couplet
| 3: both sides of a couplet
1: left side of a couplet
Incantions are no longer hanlded in rune.rs, and the task macros have
been removed. Now, a verse incants itself, matching on its own meter to
determine how to handle the next verse.
The following runes were added to help with handling STDERR:
Write2 -> 2>
WriteAll -> &>
Addendum2 -> 2>>
AddendumAll -> &>>
The 'io' field in verse was changed from an Option<Rune>, to an array of
Runes, since a single verse might have multiple IO operations.
The following fields were added to Verse, to assist with handling
STDERR:
ip -> List of filenames to read into STDIN
op -> List of filenames to send STDOUT to
ep -> List of filenames to send STDERR to
Keep track of channels when reading a poem. Channels are relating to IO
operations. If channel is None, words get pushed to the verse's primary
stanza (i.e. the verb or the clause). If a channel is selected, words
are pushed to one of the aforementioned new fields in Verse.
Read -> ip
Write/Addedum -> op
Write2/Addedum2 -> ep
WriteAll/AddendumAll -> op and ep
Notes
Notes:
This commit also added tests for the new Runes.
Diffstat (limited to 'src/poem/elements/verse.rs')
-rw-r--r-- | src/poem/elements/verse.rs | 285 |
1 files changed, 266 insertions, 19 deletions
diff --git a/src/poem/elements/verse.rs b/src/poem/elements/verse.rs index e857676..307ebc8 100644 --- a/src/poem/elements/verse.rs +++ b/src/poem/elements/verse.rs @@ -2,7 +2,13 @@ use super::rune::Rune; use super::stanza::Stanza; use super::word::Word; use crate::poem::Poem; +use libc::{waitpid, WNOHANG}; +use std::fs::OpenOptions; +use std::io::{self, Read, Write}; +use std::os::unix::process::CommandExt; use std::path::Path; +use std::process::{Command, Output, Stdio}; +use std::sync::{Arc, Mutex}; /// A [Stanza] and it's [meter](Rune) /// @@ -12,8 +18,11 @@ use std::path::Path; #[derive(Debug, Clone)] pub struct Verse { pub stanza: Stanza, - pub couplet: bool, - pub io: Rune, + pub couplet: u8, + pub io: Vec<Rune>, + pub ip: Stanza, + pub op: Stanza, + pub ep: Stanza, pub poems: Vec<Poem>, pub meter: Rune, } @@ -26,8 +35,11 @@ impl Verse { pub fn new() -> Self { Verse { stanza: Stanza::new(), - couplet: false, - io: Rune::None, + couplet: 0, + io: Vec::new(), + ip: Stanza::new(), + op: Stanza::new(), + ep: Stanza::new(), poems: Vec::new(), meter: Rune::None, } @@ -64,6 +76,8 @@ impl Verse { /// Alias to [Verse].stanza.clear() pub fn clear(&mut self) { self.stanza.clear(); + self.io.clear(); + self.poems.clear(); } /// Check if the [Verse] contains any internal poems @@ -79,25 +93,26 @@ impl Verse { /// Push a word to the [Stanza] after performing a few extra checks, such /// as whether or not the word is empty, or if the word should be /// interpreted as an environment variable. - pub fn add(&mut self, word: &mut Word) { - if !word.is_empty() { - // Push the word, and clear the stack - self.push(word.iter().collect()); - word.clear(); + pub fn add(&mut self, word: &mut Word, channel: Option<Rune>) { + // Do nothing if the stack is empty + if word.is_empty() { + return; } - } - /// Split a [Verse] into two different [Verse]s - /// - /// This is useful for [Rune::Read], [Rune::Write], and [Rune::Addendum]. - pub fn split(&mut self, c: &str) -> Vec<String> { - for (i, s) in self.stanza.iter().enumerate() { - if *s == c { - let split = self.stanza.split_off(i); - return split[1..].to_vec(); + // Push the word + match channel { + Some(Rune::Read) => self.ip.push(word.iter().collect()), + Some(Rune::Write) | Some(Rune::Addendum) => self.op.push(word.iter().collect()), + Some(Rune::Write2) | Some(Rune::Addendum2) => self.ep.push(word.iter().collect()), + Some(Rune::WriteAll) | Some(Rune::AddendumAll) => { + self.op.push(word.iter().collect()); + self.ep.push(word.iter().collect()); } + Some(_) | None => self.push(word.iter().collect()), } - vec![] + + // Clear the stack + word.clear(); } /// Check if the `verb()` exists in the `$PATH` @@ -156,4 +171,236 @@ impl Verse { // Return true if the full path or relative path exists true } + + pub fn incant( + &mut self, + out: &mut Vec<u8>, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Read files into 'out' if Rune::Read is present in the verse's IO + if self.io.contains(&Rune::Read) { + // Enable piping on stdin + self.couplet += 2; + + // Read all files specified after '<' into 'out', since there may + // also be piped output from the last command + for path in self.ip.iter() { + let mut file = OpenOptions::new().read(true).open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + out.append(&mut contents.as_bytes().to_vec()); + } + } + + // Build the command + let mut command = Command::new(self.verb()); + command.args(self.clause().unwrap_or(vec![])); + + // Determine couplet status + if self.couplet == 1 { + // Verse is the left half of a couplet + command.stdout(Stdio::piped()); + } else if self.couplet == 2 { + // Verse is the right half of a couplet + command.stdin(Stdio::piped()); + } else if self.couplet == 3 { + // Verse is taking in and piping out output + command.stdout(Stdio::piped()); + command.stdin(Stdio::piped()); + } + + // Setup for other IO + if self.io.contains(&Rune::Write) || self.io.contains(&Rune::Addendum) { + command.stdout(Stdio::piped()); + } + if self.io.contains(&Rune::Write2) || self.io.contains(&Rune::Addendum2) { + command.stderr(Stdio::piped()); + } + if self.io.contains(&Rune::WriteAll) || self.io.contains(&Rune::AddendumAll) { + command.stdout(Stdio::piped()); + command.stderr(Stdio::piped()); + } + + // Detach the process group, if in the [Rune::Quiet] meter + if self.meter == Rune::Quiet { + command.process_group(0); + } + + // Spawn the process + let mut child = command.spawn()?; + + // Pipe in command, if we're the right side of a couplet + if self.couplet > 1 { + let stdin = child.stdin.as_mut().ok_or(io::ErrorKind::BrokenPipe)?; + stdin.write_all(&out)?; + out.clear(); + } + + // Determine what to do based on the meter + let mut output: Output; + let mut err: Vec<u8> = Vec::new(); + match self.meter { + Rune::None | Rune::And | Rune::Continue => { + output = child.wait_with_output()?; + if self.io.contains(&Rune::Write) || self.io.contains(&Rune::Addendum) { + out.append(&mut output.stdout); + } + if self.io.contains(&Rune::Write2) || self.io.contains(&Rune::Addendum2) { + err.append(&mut output.stderr); + } + if self.io.contains(&Rune::WriteAll) || self.io.contains(&Rune::AddendumAll) { + out.append(&mut output.stdout); + err.append(&mut output.stderr); + } + } + Rune::Couplet => { + output = child.wait_with_output()?; + out.append(&mut output.stdout); + if self.io.contains(&Rune::Write2) || self.io.contains(&Rune::Addendum2) { + err.append(&mut output.stderr); + } + if self.io.contains(&Rune::WriteAll) || self.io.contains(&Rune::AddendumAll) { + err.append(&mut output.stderr); + } + } + Rune::Quiet => { + println!("[&] {}", child.id()); + + pids.lock().unwrap().push(child.id() as i32); + let stanza = self.stanza.join(" ").to_string(); + let pids = Arc::clone(pids); + + unsafe { + signal_hook::low_level::register(signal_hook::consts::SIGCHLD, move || { + for pid in pids.lock().unwrap().iter() { + let mut pid = *pid; + let mut status: i32 = 0; + pid = waitpid(pid, &mut status, WNOHANG); + if pid > 0 { + print!("\n[&] + done {}", stanza); + io::stdout().flush().unwrap(); + } + } + }) + .unwrap(); + } + + return Ok(0); + } + _ => unreachable!(), + } + + // Perform IO operations + let mut oi = 0; + let mut ei = 0; + self.io.retain(|rune| *rune != Rune::Read); + for io in self.io.iter() { + let (f, f2) = match *io { + Rune::Write => { + oi += 1; + ( + Some( + OpenOptions::new() + .create(true) + .write(true) + .open(&self.op[oi - 1])?, + ), + None, + ) + } + Rune::Write2 => { + ei += 1; + ( + None, + Some( + OpenOptions::new() + .create(true) + .write(true) + .open(&self.ep[ei - 1])?, + ), + ) + } + Rune::WriteAll => { + oi += 1; + ei += 1; + ( + Some( + OpenOptions::new() + .create(true) + .write(true) + .open(&self.op[oi - 1])?, + ), + Some( + OpenOptions::new() + .create(true) + .write(true) + .open(&self.ep[ei - 1])?, + ), + ) + } + Rune::Addendum => { + oi += 1; + ( + Some( + OpenOptions::new() + .create(true) + .append(true) + .open(&self.op[oi - 1])?, + ), + None, + ) + } + Rune::Addendum2 => { + ei += 1; + ( + None, + Some( + OpenOptions::new() + .create(true) + .append(true) + .open(&self.ep[ei - 1])?, + ), + ) + } + Rune::AddendumAll => { + oi += 1; + ei += 1; + ( + Some( + OpenOptions::new() + .create(true) + .append(true) + .open(&self.op[oi - 1])?, + ), + Some( + OpenOptions::new() + .create(true) + .append(true) + .open(&self.ep[ei - 1])?, + ), + ) + } + _ => unreachable!(), + }; + + match f { + Some(mut file) => file.write(out)?, + None => 0, + }; + + match f2 { + Some(mut file) => file.write(&err)?, + None => 0, + }; + } + + if !output.status.success() { + return Ok(output.status.code().unwrap_or(-1)); + } + + out.clear(); + err.clear(); + + Ok(output.status.code().unwrap_or(0)) + } } |