summaryrefslogtreecommitdiffstats
path: root/src/recite.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/recite.rs')
-rw-r--r--src/recite.rs628
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))
+ }
+}