diff options
Diffstat (limited to 'src/poem/elements')
-rw-r--r-- | src/poem/elements/rune.rs | 363 | ||||
-rw-r--r-- | src/poem/elements/stanza.rs | 10 | ||||
-rw-r--r-- | src/poem/elements/verse.rs | 159 | ||||
-rw-r--r-- | src/poem/elements/word.rs | 2 |
4 files changed, 534 insertions, 0 deletions
diff --git a/src/poem/elements/rune.rs b/src/poem/elements/rune.rs new file mode 100644 index 0000000..fc2b27a --- /dev/null +++ b/src/poem/elements/rune.rs @@ -0,0 +1,363 @@ +use super::verse::Verse; +use crate::iobtask; +use crate::{btask, ctask, task}; +use core::fmt; +use libc::waitpid; +use libc::WNOHANG; +use std::fs::OpenOptions; +use std::io::{self, Read, Write}; +use std::os::unix::process::CommandExt; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; + +/// Describes one or two characters from the input +/// +/// [Rune]s are a way to mark special characters from the input string (i.e. +/// poetry). Some [Rune]s are special--as they denote the end of a [Verse]-- +/// and are refered to as a Meter. For instance, `Addendum`, `Couplet`, +/// `Quiet`, and `And`, are all meters. Meters also determine how the +/// [Stanza][super::stanza::Stanza] should be interpreted. For instance, a +/// [Stanza][super::stanza::Stanza] that is piped needs to have +/// its `STDOUT` captured (rather than printing out to the terminal), and +/// subsequently sent to the next [Verse] in the [Poem][super::super::Poem]. +/// +/// # Values +/// * `None` - A shell command with no additional actions (the end of a poem) +/// * `Pause` - The space character, to dilineate words (` `) +/// * `Path` - The forward slash character, to dilineate paths (`/`) +/// * `Env` - Indicates an environment variable (`$`) +/// * `String` - Interpret all character as one large +/// [Word][super::word::Word] (`'` or `"`) +/// * `Poem` - A subcommand to run first (`\``) +/// * `Read` - Read files into STDIN (`<`) +/// * `Write` - Write STDOUT to a file (`>`) +/// * `Addendum` - Append STDOUT to a file (`>>`) +/// * `Couplet` - Pipe the output of this command into the next (`|`) +/// * `Quiet` - Fork the called process into the background (`&`) +/// * `And` - Run the next command only if this one succeeds (`&&`) +/// * `Continue` - String commands together on a single line (`;`) +/// * `Home` - Interpret `~` as `$HOME` +/// * `Else` - Any other character +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Rune { + None, // No meter (the end of a poem) + Pause, // A space + Path, // A forward slash + String, // Interpret the following as one large [Word] + Poem, // Run a sub-poem before the main one + Read, // Read files into STDIN + Write, // Send STDOUT to a file + Addendum, // Append STDOUT to a file + Couplet, // Pipe the output of this command into the next + Quiet, // Fork the command into the background + And, // Run the next command only if this succeeds + Continue, // Run the next command, even if this doesn't succeed + Home, // Interpret '~' as $HOME + Else, // Any other character +} + +impl fmt::Display for Rune { + /// Determine how to print out a [Rune] + /// + /// Each [Rune]'s symbol corresponds to its input. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let rune = match self { + Rune::None => "", + Rune::Pause => " ", + Rune::Path => "/", + Rune::String => "\"", + Rune::Poem => "`", + Rune::Read => "<", + Rune::Write => ">", + Rune::Addendum => ">>", + Rune::Couplet => "|", + Rune::Quiet => "&", + Rune::And => "&&", + Rune::Continue => ";", + Rune::Home => "~", + Rune::Else => "_", + }; + + write!(f, "{}", rune) + } +} + +impl Rune { + // /// Check if a character is a special [Rune] + // pub fn special(rune: char) -> bool { + // match rune { + // ' ' | '/' | '$' | '\'' | '"' | '`' | '<' | '>' | '|' | '&' | ';' | '~' => true, + // _ => false, + // } + // } + + /// Recite a verse with [Rune::None] + /// + /// Call this function on a [Verse] with a meter of type [Rune::None]. + /// This forks into a child process, calls the `verb()` (i.e. program) + /// that was specified in the [Verse], then waits for that program to + /// complete. If the last [Verse] piped its contents into `out`, it will + /// be piped into the STDIN of this [Verse]. If all Rust code is called + /// successfully, return the exit code of the process. Otherwise, return a + /// [std::io::Error]. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `out: &mut String` - A string that may have output from the last command + pub fn incant_none(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + let child = task!(verse, out); + + let output = child.wait_with_output()?; + + if !output.status.success() { + return Ok(output.status.code().unwrap_or(-1)); + } + + Ok(output.status.code().unwrap_or(0)) + } + + /// Recite a verse with [Rune::Couplet] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Couplet]. + /// This forks into a child process, calls the `verb` (i.e. program) + /// that was specified in the [Verse], then waits for that program to + /// complete. If the last [Verse] piped its contents into `out`, it will + /// be piped into the STDIN of this [Verse]. Then, the contents of this + /// processes' STDOUT are stored in `out`. If all Rust code is called + /// successfully, return the exit code of the process. Otherwise, return a + /// [std::io::Error]. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `out: &mut String` - A string that may have output from the last command + pub fn incant_couplet(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + let child = ctask!(verse, out); + + let output = child.wait_with_output()?; + + if !output.status.success() { + return Ok(output.status.code().unwrap_or(-1)); + } + + out.push_str( + String::from_utf8_lossy(&output.stdout) + .into_owned() + .as_str(), + ); + + Ok(output.status.code().unwrap_or(0)) + } + + /// Recite a verse with [Rune::Quiet] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Quiet]. + /// This forks a child process into the background. It then registers a + /// `SIGCHLD` handler, making sure to do so for each PID in the `pids` + /// Vec. If the last [Verse] piped its contents into `out`, it will be + /// piped into the STDIN of this [Verse]. If all Rust code is called + /// successfully, return the exit code of the process. Otherwise, return a + /// [std::io::Error]. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `out: &mut String` - A string that may have output from the last command + /// * `pids: Arc<Mutex<Vec<i32>>>` - A vector that stores the PIDs of all background processes that belong to the shell + pub fn incant_quiet( + verse: &Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + let child = btask!(verse, out); + println!("[&] {}", child.id()); + + pids.lock().unwrap().push(child.id() as i32); + let stanza = verse.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(); + } + + Ok(0) + } + + /// Alias to [Rune::incant_none] + pub fn incant_and(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + Rune::incant_none(verse, out) + } + + /// Alias to [Rune::incant_none] + pub fn incant_continue(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + Rune::incant_none(verse, out) + } + + /// Recite a verse with [Rune::Read] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Read]. + /// This reads the specified files into `out`, then makes a call to + /// [Rune::incant_none] with all the contents of `out`. Anything piped to + /// this command will appear in `out` first, and any subsequent files will + /// be appended. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `paths: &Verse` - The next verse (i.e. the file paths) + /// * `out: &mut String` - A string that may have output from the last command, + /// and that will be used to store the contents of the + /// file paths in `next` + pub fn incant_read( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // Read all file specified in the next verse into 'out', since there + // may also be piped output from the last command + for path in paths.iter() { + let mut file = OpenOptions::new().read(true).open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + out.push_str(contents.as_str()); + } + + // Alias incant_<meter> + match verse.meter { + Rune::None => Rune::incant_none(&verse, out), + Rune::Couplet => Rune::incant_couplet(&verse, out), + Rune::Quiet => Rune::incant_quiet(&verse, out, pids), + Rune::And => Rune::incant_and(&verse, out), + Rune::Continue => Rune::incant_continue(&verse, out), + _ => unreachable!(), + } + } + + /// Recite a verse with [Rune::Write] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Write]. + /// This writes the output of the verse into the specified files, after + /// making a call to [Rune::incant_couplet]. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `paths: &Verse` - The next verse (i.e. the file paths) + /// * `out: &mut String` - A string that may have output from the last command, + /// and that will be used to store the contents of the + /// file paths in `next` + pub fn incant_write( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // Alias incant_<meter> + // let status = Rune::incant_couplet(&verse, out)?; + let status = match verse.meter { + Rune::None => Rune::incant_none(&verse, out)?, + Rune::Couplet => Rune::incant_couplet(&verse, out)?, + Rune::Quiet => Rune::incant_quiet_io(&verse, out, pids)?, + Rune::And => Rune::incant_and(&verse, out)?, + Rune::Continue => Rune::incant_continue(&verse, out)?, + _ => unreachable!(), + }; + + // Write output to each file specified in the next verse + for path in paths.iter() { + let mut file = OpenOptions::new().create(true).write(true).open(path)?; + file.write(out.as_bytes())?; + } + + // Clear out + out.clear(); + + // Return the exit status + Ok(status) + } + + /// Recite a verse with [Rune::Addendum] + /// + /// Same as [Rune::Write], except it appends to the file(s) specified, + /// instead of overwriting them. + /// + /// # Arguments + /// * `verse: &Verse` - The verse to recite + /// * `paths: &Verse` - The next verse (i.e. the file paths) + /// * `out: &mut String` - A string that may have output from the last command, + /// and that will be used to store the contents of the + /// file paths in `next` + pub fn incant_addendum( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // Alias incant_<meter> + // let status = Rune::incant_couplet(&verse, out)?; + let status = match verse.meter { + Rune::None => Rune::incant_none(&verse, out)?, + Rune::Couplet => Rune::incant_couplet(&verse, out)?, + Rune::Quiet => Rune::incant_quiet_io(&verse, out, pids)?, + Rune::And => Rune::incant_and(&verse, out)?, + Rune::Continue => Rune::incant_continue(&verse, out)?, + _ => unreachable!(), + }; + + // Write output to each file specified in the next verse + for path in paths.iter() { + let mut file = OpenOptions::new().create(true).append(true).open(path)?; + file.write(out.as_bytes())?; + } + + // Clear out + out.clear(); + + // Return the exit status + Ok(status) + } + + pub fn incant_quiet_io( + verse: &Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + let child = iobtask!(verse, out); + println!("[&] {}", child.id()); + + pids.lock().unwrap().push(child.id() as i32); + let stanza = verse.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(); + } + + Ok(0) + } +} diff --git a/src/poem/elements/stanza.rs b/src/poem/elements/stanza.rs new file mode 100644 index 0000000..d58d080 --- /dev/null +++ b/src/poem/elements/stanza.rs @@ -0,0 +1,10 @@ +/// The actionable part of a [Verse][super::verse::Verse] +/// +/// Each [Stanza] has two parts, a `verb()` and a `clause()`. The `verb()` is +/// the program, or path to the program to call, while the `clause()` contains +/// arguments to pass to that program. +/// +/// The [Stanza] is just stored as a [Vec] of [String]s, where the verb is the +/// first entry in the vector (i.e. `stanza[0]`) and the clause the the +/// remainder of the vector (i.e. `stanza[1..]`). +pub type Stanza = Vec<String>; diff --git a/src/poem/elements/verse.rs b/src/poem/elements/verse.rs new file mode 100644 index 0000000..e857676 --- /dev/null +++ b/src/poem/elements/verse.rs @@ -0,0 +1,159 @@ +use super::rune::Rune; +use super::stanza::Stanza; +use super::word::Word; +use crate::poem::Poem; +use std::path::Path; + +/// A [Stanza] and it's [meter](Rune) +/// +/// In addition to a [Stanza] and a [meter](Rune), this also holds a [bool] +/// value called `couplet`, indicating that it needs to accept input on `STDIN` +/// from the previous [Verse]. +#[derive(Debug, Clone)] +pub struct Verse { + pub stanza: Stanza, + pub couplet: bool, + pub io: Rune, + pub poems: Vec<Poem>, + pub meter: Rune, +} + +impl Verse { + /// Create a new [Verse] + /// + /// Returns a new [Verse], with an empty [Stanza], a meter of [Rune::None], + /// and `couplet` set to `false`. + pub fn new() -> Self { + Verse { + stanza: Stanza::new(), + couplet: false, + io: Rune::None, + poems: Vec::new(), + meter: Rune::None, + } + } + + /// Get the [Verse]'s verb + /// + /// Return the program to be forked + pub fn verb(&self) -> String { + self.stanza[0].clone() + } + + /// Get the [Verse]'s clause + /// + /// Return program arguments, if they exist + pub fn clause(&self) -> Option<Vec<String>> { + match self.stanza.len() { + 0 => None, + 1 => None, + _ => Some(self.stanza[1..].to_vec()), + } + } + + /// Alias to [Verse].stanza.push() + pub fn push(&mut self, word: String) { + self.stanza.push(word); + } + + /// Alias to [Verse].stanza.is_empty() + pub fn is_empty(&self) -> bool { + self.stanza.is_empty() + } + + /// Alias to [Verse].stanza.clear() + pub fn clear(&mut self) { + self.stanza.clear(); + } + + /// Check if the [Verse] contains any internal poems + pub fn poems(&self) -> bool { + if self.poems.len() > 0 { + return true; + } + false + } + + /// Push a word to the [Verse]'s [Stanza] + /// + /// 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(); + } + } + + /// 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(); + } + } + vec![] + } + + /// Check if the `verb()` exists in the `$PATH` + /// + /// First checks if the `verb()` is a relative or full path. If it is, + /// check whether or not it exists. If it does exist, return true, + /// otherwise seeif the `verb()` is cached in our list of binaries. Search is + /// done in $PATH order. + /// + /// # Examples + /// ``` + /// let bins = vec!["cargo", "ruby", "cat"] + /// .into_iter() + /// .map(String::from) + /// .collect<Vec<String>>(); + /// + /// let command_success = vec!["cargo", "build", "--release"] + /// .into_iter() + /// .map(String::from) + /// .collect<Vec<String>>(); + /// + /// let command_fail = vec!["make", "-j8"] + /// .into_iter() + /// .map(String::from) + /// .collect<Vec<String>>(); + /// + /// let stanza_success = Stanza::new(command_success); + /// let stanza_fail = Stanza::new(command_fail); + /// + /// stanza_success.spellcheck(bins) // -> true + /// stanza_fail.spellcheck(bins) // -> false + /// ``` + pub fn spellcheck(&self, bins: &Vec<String>) -> bool { + // An empty verb (i.e. the empty string) cannot be a program, so + // return false + // Thanks to the parsing in Poem::read, however, it's + // unlikely for this to happen + if self.verb().is_empty() { + return false; + } + + // Only search the $PATH if a full or relative path was not given, or + // if the path given does not exist + if !Path::new(self.verb().as_str()).exists() { + // Try to find a binary in our path with the same name as the verb + // Searches in $PATH order + match bins + .iter() + .find(|bin| bin.split('/').last().unwrap() == self.verb()) + { + Some(_) => return true, + None => return false, + } + } + + // Return true if the full path or relative path exists + true + } +} diff --git a/src/poem/elements/word.rs b/src/poem/elements/word.rs new file mode 100644 index 0000000..dd088e0 --- /dev/null +++ b/src/poem/elements/word.rs @@ -0,0 +1,2 @@ +/// A (typically) space dilineated piece of a [Stanza][super::stanza::Stanza] +pub type Word = Vec<char>; |