diff options
author | Rory Dudley | 2024-02-22 23:12:24 -0700 |
---|---|---|
committer | Rory Dudley | 2024-02-22 23:12:24 -0700 |
commit | 536e250653e5c140a6d9e60f1cd652b79135e7a6 (patch) | |
tree | 6a6b212c913f52d559ed9ea8a9477bebb9b38a2d /src/recite.rs | |
parent | a14fdf8faa85628baf06399961eb1f9ab4c5f3eb (diff) | |
download | dwarvish-536e250653e5c140a6d9e60f1cd652b79135e7a6.tar.gz |
Reorganization and comments
Broke out the structs for a poem into their own file: src/recite.rs.
Also put the 'prefresh' function into it's own file: src/recite/path.rs.
Commented most of the parser code (including structs and helper methods
related to parsing (i.e. Verse, Stanza, Meter, Poem)). Renamed any
instance of the 'paths' variable to 'path'.
Notes
Notes:
The biggest task now is to cleanup Poem::recite. It has a ton of bogus
error messages, and (seemingly) redundant code.
Diffstat (limited to 'src/recite.rs')
-rw-r--r-- | src/recite.rs | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/src/recite.rs b/src/recite.rs new file mode 100644 index 0000000..4e1dc1f --- /dev/null +++ b/src/recite.rs @@ -0,0 +1,628 @@ +pub mod path; +use core::fmt; +use path::prefresh; +use std::io::{self, Write}; +use std::path::Path; +use std::process::{exit, Command, Stdio}; + +/// Describes the ending of a [Verse] +/// +/// The ending of a verse determines how the [Stanza] should be interpreted. +/// For instance, a [Stanza] that is piped needs to have it's `STDOUT` +/// captured (rather than printing out to the terminal), and subsequently sent +/// to the next [Verse] in the [Poem]. +/// +/// # Values +/// * `None` - A shell command with no additional actions +/// * `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 (`&&`) +/// * `String` - String commands together on a single line (`;`) +#[derive(Debug)] +enum Meter { + None, // No meter + 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 + String, // Run the next command, even if this doesn't succeed +} + +impl fmt::Display for Meter { + /// Determine how to print out a [Meter] + /// + /// Each [meter's][Meter] symbol corresponds to it's input. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let meter = match self { + Meter::None => "", + Meter::Couplet => "|", + Meter::Quiet => "&", + Meter::And => "&&", + Meter::String => ";", + }; + + write!(f, "{}", meter) + } +} + +/// Holds a program to be called +/// +/// This is simply the first word in a full command [String], dilineated via +/// whitespace. +type Verb = String; + +/// Holds arguments to a program +/// +/// This is a list of all the words that come after the [Verb], dilineated via +/// whitespace. +type Clause = Vec<String>; + +/// Holds the interpreted elements of a [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. +#[derive(Debug)] +struct Stanza { + verb: Verb, + clause: Clause, +} + +impl fmt::Display for Stanza { + /// Print out a [Stanza] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.verb, self.clause.join(" ")) + } +} + +impl Stanza { + /// Create a new [Stanza] + /// + /// Returns a new [Stanza] built from a `Vec<String>`. The first element of + /// the vector becomes the [Verb], while the remainder of the vector + /// becomes the [Clause]. + /// + /// # Arguments + /// `stanza: Vec<String>` - The full command split into individual strings + /// via whitespace + /// + /// # Examples + /// ``` + /// // Input: cargo build --release + /// let command = vec!["cargo", "build", "--release"] + /// .into_iter() + /// .map(String::from) + /// .collect<Vec<String>>(); + /// let stanza = Stanza::new(command); + /// println!("{}", stanza.verb); + /// println!("{:?}", stanza.clause); + /// + /// ``` + fn new(stanza: Vec<String>) -> Stanza { + Stanza { + verb: stanza[0].to_owned(), + clause: stanza[1..].to_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 see + /// if 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 + /// ``` + 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 + } +} + +/// Holds a [Stanza] and its [Meter] +/// +/// In addition to a [Stanza] and a [Meter], [verse's][Verse] also hold a bool +/// value called `couplet`, indicating that it needs to accept input on `STDIN` +/// from the previous [Verse]. +#[derive(Debug)] +struct Verse { + stanza: Stanza, + meter: Meter, + couplet: bool, +} + +impl fmt::Display for Verse { + /// Print out a [Verse] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {}", + self.verb(), + self.clause().join(" "), + self.meter + ) + } +} + +impl Verse { + /// Create a new [Verse] + /// + /// Returns a new [Verse] built from a [Stanza], a [Meter], and a `couplet` + /// indicator. See [Poem::read] for more details on how these are + /// constructed. + fn new(stanza: Stanza, meter: Meter, couplet: bool) -> Verse { + Verse { + stanza, + meter, + couplet, + } + } + + /// Alias to [Stanza::spellcheck] + fn spellcheck(&self, bins: &Vec<String>) -> bool { + self.stanza.spellcheck(bins) + } + + /// Alias to [stanza's][Stanza] `verb` + fn verb(&self) -> String { + self.stanza.verb.clone() + } + + /// Alias to [stanza's][Stanza] `clause` + fn clause(&self) -> Vec<String> { + self.stanza.clause.clone() + } +} + +/// An entire shell command parsed into [verse's][Verse] +/// +/// A [Poem] is the structure that contains a full shell command/program. It +/// may be composed of one or many [verse's][Verse]. +#[derive(Debug)] +pub struct Poem { + verses: Vec<Verse>, +} + +impl Poem { + /// Create a new [Poem] + /// + /// Returns a new [Poem] built from a list of [verse's][Verse]. + fn new(verses: Vec<Verse>) -> Poem { + Poem { verses } + } + + /// Recite a [Poem] (run the shell command(s)/program) + /// + /// This function attempts to call each [Verse] in the [Poem], in the order + /// that it was inputted/parsed. + /// + /// # Arguments + /// * `path` - A list of directories from the $PATH environment variable + /// Needed in case we need to refresh the $PATH + /// * `bins` - A list of binaries cached from the $PATH, used for searching + /// for a program that matches the verb in each [Verse] + /// + /// # Returns + /// * `true` - If the entire [Poem] was recited without fault + /// * `false` - If any [Verse] of the [Poem] was invalid + /// + /// # Examples + /// ``` + /// let poetry = "ps aux | grep dwvsh".to_string(); + /// let poem = Poem::read(poetry); + /// + /// match poem { + /// Some(poem) => { poem.recite(path, &mut bins); } + /// None => {} + /// } + /// ``` + pub fn recite(&self, path: &Vec<&Path>, bins: &mut Vec<String>) -> bool { + // Variable for storing the output of a piped verse + let mut out: String = String::new(); + + // Loop through each verse in the poem + for verse in self.verses.iter() { + // Check if user wants to exit the shell + if verse.verb() == "exit" || verse.verb() == "quit" { + exit(0); + } + + // Check if the user wants to change directories + if verse.verb() == "cd" { + let path: String; + if verse.clause().is_empty() { + path = env!("HOME").to_string(); + } else { + path = verse.clause().first().unwrap().to_owned(); + } + + match std::env::set_current_dir(&path) { + Ok(_) => continue, + Err(_) => { + println!("cd: unable to change into {}", path); + continue; + } + } + } + + // Check if the verb exists + // If it doesn't exist, try refreshing the binary cache, and check + // again + // If it still doesn't exist, print an error + if !verse.spellcheck(bins) { + *bins = prefresh(path); + if !verse.spellcheck(bins) { + println!("dwvsh: {}: command not found", verse.verb()); + continue; + } + } + + // If the verse is a couplet, it means it needs the output of the + // previous verse on `STDIN` + if verse.couplet { + match verse.meter { + Meter::Couplet => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("dwvsh: error 0"); + + let stdin = child.stdin.as_mut().expect("dwvsh: error 6"); + stdin.write_all(&out.as_bytes()).expect("dwvsh: error 7"); + out.clear(); + + let output = child.wait_with_output().unwrap(); + out = String::from_utf8_lossy(&output.stdout).to_string(); + } + Meter::Quiet => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .stdin(Stdio::piped()) + .spawn() + .expect("dwvsh: error 1"); + + let stdin = child.stdin.as_mut().expect("dwvsh: error 8"); + stdin.write_all(&out.as_bytes()).expect("dwvsh: error 9"); + out.clear(); + + print!("[f] {}", child.id()); + std::thread::spawn(move || { + child.wait().unwrap(); + println!("[f] +done {}", child.id()); + io::stdout().flush().unwrap(); + }); + } + Meter::String => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .spawn() + .expect("dwvsh: error 5"); + + let stdin = child.stdin.as_mut().expect("dwvsh: error 8"); + stdin.write_all(&out.as_bytes()).expect("dwvsh: error 9"); + out.clear(); + + child.wait().unwrap(); + } + Meter::And | Meter::None => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .stdin(Stdio::piped()) + .spawn() + .expect("dwvsh: error 2"); + + let stdin = child.stdin.as_mut().expect("dwvsh: error 10"); + stdin.write_all(&out.as_bytes()).expect("dwvsh: error 11"); + out.clear(); + + if !child.wait().unwrap().success() { + break; + } + } + }; + } else { + match verse.meter { + Meter::Couplet => { + let child = Command::new(verse.verb()) + .args(verse.clause()) + .stdout(Stdio::piped()) + .spawn() + .expect("dwvsh: error 3"); + + let output = child.wait_with_output().unwrap(); + out = String::from_utf8_lossy(&output.stdout).to_string(); + } + Meter::Quiet => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .spawn() + .expect("dwvsh: error 4"); + + println!("[f] {}", child.id()); + std::thread::spawn(move || { + child.wait().unwrap(); + print!("[f] +done {}\n", child.id()); + io::stdout().flush().unwrap(); + }); + } + Meter::String => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .spawn() + .expect("dwvsh: error 5"); + + child.wait().unwrap(); + } + Meter::And | Meter::None => { + let mut child = Command::new(verse.verb()) + .args(verse.clause()) + .spawn() + .expect("dwvsh: error 5"); + + if !child.wait().unwrap().success() { + break; + } + } + }; + } + } + + // If we've successfully exited the loop, then all verse's were + // properly recited + true + } + + /// Parse a [Poem] from a raw [String] input + /// + /// Takes a shell command/program and converts it to a machine-runnable + /// [Poem]. If there is a parse error, [Poem::read] may [Option]ally return + /// `None`. As of now, there is no support for multiline programs, unless + /// newlines (`\n`) were to be swapped out for semicolons (`;`) before + /// calling this function. See [Poem::recite] for how each [Verse] in a + /// [Poem] is called. + /// + /// # Examples + /// ``` + /// let poetry = "ps aux | grep dwvsh".to_string(); + /// let poem = Poem::read(poetry); + /// ``` + pub fn read(poetry: String) -> Option<Poem> { + // Need to loop through each char in the input string, since some + // characters aren't whitespace dilineated (`;`, `&`, etc.) + // + // Need to keep track of the previous verse, since it might haver + // a Meter of Couplet, meaning that we need to set couplet on the + // current verse + let mut chars = poetry.chars(); + let mut verses: Vec<Verse> = Vec::new(); // Accumulate verses + let mut stanza: Vec<String> = Vec::new(); // Stack for each stanza + let mut word: Vec<char> = Vec::new(); // Stack for each word + let mut prev: Option<&Verse> = None; // The previous verse + + // Parse from left to right + loop { + // Get the next character in the input string + let char = chars.next(); + + // Check if the previous verse is piping output to current + // TODO: Don't need to run this on each iteration of the loop, just when + // a stanza is pushed to a verse + let couplet = match prev { + Some(prev) => match prev.meter { + Meter::Couplet => true, + Meter::Quiet | Meter::And | Meter::String | Meter::None => false, + }, + None => false, + }; + + // Check if the previous verse was metered + // Need this to check for parse/input errors + let metered = match prev { + Some(prev) => match prev.meter { + Meter::Couplet | Meter::Quiet | Meter::And | Meter::String => true, + Meter::None => false, + }, + None => false, + }; + + // Do something depending on what the character is + match char { + // Print an error, and return None if a Meter was used without + // a Stanza before it + Some(meter) + if (meter == '|' || meter == '&' || meter == ';') + && metered + && stanza.is_empty() => + { + // TODO: Add more verbose error message + println!("dwvsh: parse error"); + return None; + } + + // The character represents the Couplet Meter + Some(meter) if meter == '|' => { + // If there are chars on the word stack, push that word + // onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Couplet, + couplet, + )); + + // Clear the stacks + stanza.clear(); + word.clear(); + } + + // The character represents the Quiet (or And) Meter + Some(meter) if meter == '&' => { + // If there are chars on the word stack, push that word + // onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // Need to peek at the next character, since '&' can mean + // Meter::Quiet, or '&&' can mean Meter::And + match chars.clone().peekable().peek() { + // Indicated Meter::And + Some(c) if c == &'&' => { + // Pop the next character from the input string + // (since we know what it is) + chars.next(); + + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::And, + couplet, + )); + } + + // Indicates Meter::Quiet + Some(_) => { + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Quiet, + couplet, + )); + } + + // Indicated the end of the input + None => { + // The end of input also indicates the end of a + // verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Quiet, + couplet, + )); + + // We can break out of the loop here, since it's + // the end of the raw input + break; + } + } + + // Clear the stacks + stanza.clear(); + word.clear(); + } + + // The character represents the String Meter + Some(meter) if meter == ';' => { + // If there are chars on the word stack, push that word + // onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::String, + couplet, + )); + + stanza.clear(); + word.clear(); + } + + // The character is whitespace + Some(char) if char == ' ' || char == '\t' => { + if !word.is_empty() { + stanza.push(word.iter().collect()); + word.clear(); + } + } + + // The character is any other utf8 glyph + Some(char) => { + word.push(char); + } + + // Indicates the end of the list of characters + None => { + // Always push the last word onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // Only push the stanza into a verse if it contains + // any words + if !stanza.is_empty() { + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::None, + couplet, + )); + } + + // Break from the loop, since we are out of chars + break; + } + } + + // Set previous verse to the verse that was just pushed at the end + // of each loop + prev = match verses.last() { + Some(verse) => Some(verse), + None => None, + }; + } + + // Return the (parsed) poem + Some(Poem::new(verses)) + } +} |