diff options
Diffstat (limited to 'src/recite.rs')
-rw-r--r-- | src/recite.rs | 1043 |
1 files changed, 0 insertions, 1043 deletions
diff --git a/src/recite.rs b/src/recite.rs deleted file mode 100644 index 07e6276..0000000 --- a/src/recite.rs +++ /dev/null @@ -1,1043 +0,0 @@ -mod parse; -pub mod path; -mod ps; -use crate::{btask, ctask, push, push1, task}; -use core::fmt; -use libc::{waitpid, WNOHANG}; -use path::prefresh; -use std::fs::OpenOptions; -use std::io::{self, Read, Write}; -use std::os::unix::process::CommandExt; -use std::path::Path; -use std::process::{exit, Command, Stdio}; -use std::sync::{Arc, Mutex}; - -/// 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 (`;`) -/// * `Read` - Read files into STDIN (`<`) -/// * `Write` - Write STDOUT to a file (`>`) -/// * `Addendum` - Append STDOUT to a file (`>>`) -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -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 - Read, // Read files into STDIN - Write, // Send STDOUT to a file - Addendum, // Append STDOUT to a file -} - -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 => ";", - Meter::Read => "<", - Meter::Write => ">", - Meter::Addendum => ">>", - }; - - write!(f, "{}", meter) - } -} - -impl Meter { - /// Recite a verse with [Meter::None] - /// - /// Call this function on a [Verse] with a meter of type [Meter::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 - 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 [Meter::None] - /// - /// Call this function on a [Verse] with a meter of type [Meter::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]. 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 - 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 [Meter::Quiet] - /// - /// Call this function on a [Verse] with a meter of type [Meter::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 - 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.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 [Meter::incant_none] - fn incant_and(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { - Meter::incant_none(verse, out) - } - - /// Alias to [Meter::incant_none] - fn incant_string(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { - Meter::incant_none(verse, out) - } - - /// Recite a verse with [Meter::Read] - /// - /// Call this function on a [Verse] with a meter of type [Meter::Read]. - /// This reads the specified files into `out`, then makes a call to - /// [Meter::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` - fn incant_read(verse: &Verse, paths: &Verse, out: &mut String) -> Result<i32, io::Error> { - // 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.stanza().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_none - Meter::incant_none(verse, out) - } - - /// Recite a verse with [Meter::Write] - /// - /// Call this function on a [Verse] with a meter of type [Meter::Write]. - /// This writes the output of the verse into the specified files, after - /// making a call to [Meter::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` - fn incant_write(verse: &Verse, paths: &Verse, out: &mut String) -> Result<i32, io::Error> { - // Alias incant_couplet - let status = Meter::incant_couplet(verse, out)?; - - // Write output to each file specified in the next verse - for path in paths.stanza().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 [Meter::Addendum] - /// - /// Same as [Meter::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` - fn incant_addendum(verse: &Verse, paths: &Verse, out: &mut String) -> Result<i32, io::Error> { - // Alias incant_couplet - let status = Meter::incant_couplet(verse, out)?; - - // Write output to each file specified in the next verse - for path in paths.stanza().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) - } -} - -/// 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, - rw: 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, rw: bool) -> Verse { - Verse { - stanza, - meter, - couplet, - rw, - } - } - - /// 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() - } - - /// Return the entire [stanza][Stanza] - fn stanza(&self) -> Vec<String> { - let mut list = vec![self.stanza.verb.clone()]; - list.extend(self.stanza.clause.clone()); - list - } - - /// Check if this verse is piping output - fn couplet(verse: Option<&Verse>) -> bool { - match verse { - Some(verse) => match verse.meter { - Meter::Couplet => true, - Meter::None - | Meter::Quiet - | Meter::And - | Meter::String - | Meter::Read - | Meter::Write - | Meter::Addendum => false, - }, - None => false, - } - } - - /// Check if this verse is reading from or writing to a file - fn rw(verse: Option<&Verse>) -> bool { - match verse { - Some(verse) => match verse.meter { - Meter::Read | Meter::Write | Meter::Addendum => true, - Meter::None | Meter::Couplet | Meter::Quiet | Meter::And | Meter::String => false, - }, - None => false, - } - } - - /// Check if this verse has a meter - fn cadence(verse: Option<&Verse>) -> bool { - match verse { - Some(verse) => match verse.meter { - Meter::Couplet - | Meter::Quiet - | Meter::And - | Meter::String - | Meter::Read - | Meter::Write - | Meter::Addendum => true, - Meter::None => false, - }, - None => false, - } - } -} - -/// 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>) -> Result<(), io::Error> { - // Variable for storing the output of a piped verse - let mut out: String = String::new(); - let mut pids: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new())); - - // Loop through each verse in the poem - for (i, verse) in self.verses.iter().enumerate() { - // Don't perform any actions on a verse if it's for Meter::Read or - // Meter::Write - if verse.rw { - continue; - } - - // 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; - } - } - - let mut meter = verse.meter; - - // Incant the verse, based on its meter - let status = match meter { - Meter::None => Meter::incant_none(verse, &mut out)?, - Meter::Couplet => Meter::incant_couplet(verse, &mut out)?, - Meter::Quiet => Meter::incant_quiet(verse, &mut out, &mut pids)?, - Meter::And => Meter::incant_and(verse, &mut out)?, - Meter::String => Meter::incant_string(verse, &mut out)?, - Meter::Read => { - // The parser will detect if a Read/Write/Addendum is - // missing a list of files, meaning we should always - // be able to access verses at i + 1 - let status = match Meter::incant_read(verse, &self.verses[i + 1], &mut out) { - Ok(status) => status, - Err(e) => { - eprintln!("dwvsh: {}", e.to_string().to_lowercase()); - meter = self.verses[i + 1].meter; - 1 - } - }; - - status - } - Meter::Write => { - // The parser will detect if a Read/Write/Addendum is - // missing a list of files, meaning we should always - // be able to access verses at i + 1 - let status = match Meter::incant_write(verse, &self.verses[i + 1], &mut out) { - Ok(status) => status, - Err(e) => { - eprintln!("dwvsh: {}", e.to_string().to_lowercase()); - meter = self.verses[i + 1].meter; - 1 - } - }; - - status - } - Meter::Addendum => { - // The parser will detect if a Read/Write/Addendum is - // missing a list of files, meaning we should always - // be able to access verses at i + 1 - let status = match Meter::incant_addendum(verse, &self.verses[i + 1], &mut out) - { - Ok(status) => status, - Err(e) => { - eprintln!("dwvsh: {}", e.to_string().to_lowercase()); - meter = self.verses[i + 1].meter; - 1 - } - }; - - status - } - }; - - // Don't continue reciting if there was an error, unless the meter - // is String (indicating that errors should be ignored) - if meter != Meter::String && status != 0 { - break; - } - } - - // If we've successfully exited the loop, then all verse's were - // properly recited - Ok(()) - } - - /// 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 - let mut i: usize = 0; // Keep track of our index into chars - - // Parse from left to right - loop { - // Get the next character in the input string - let char = chars.next(); - - // 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 == '&') - && Verse::cadence(prev) - && stanza.is_empty()) - || ((meter == '|' || meter == '&') && i == 0) => - { - eprintln!( - "dwvsh: parse error: verse must have a stanza: rune {} at column {}", - meter, i - ); - return None; - } - - // The character represents the Couplet Meter - Some(meter) if meter == '|' => { - push!( - word, - stanza, - Verse::couplet(prev), - prev, - verses, - Meter::Couplet - ); - } - - // The character represents the Quiet (or And) Meter - Some(meter) if meter == '&' => { - push1!( - word, - stanza, - chars, - prev, - verses, - Meter::Quiet, - '&', - Meter::And - ); - } - - // The character represents the String Meter - Some(meter) if meter == ';' => { - push!( - word, - stanza, - Verse::couplet(prev), - prev, - verses, - Meter::String - ); - } - - // The character represents the Read Meter - Some(meter) if meter == '<' => { - push!(word, stanza, true, prev, verses, Meter::Read); - } - - // The character represents the Write Meter - Some(meter) if meter == '>' => { - push1!( - word, - stanza, - chars, - prev, - verses, - Meter::Write, - '>', - Meter::Addendum - ); - } - - // The character is a newline (may happen if parsing from a file) - Some(char) if char == '\n' => { - push!( - word, - stanza, - Verse::couplet(prev), - prev, - verses, - Meter::String - ); - } - - // The character is whitespace - Some(char) if char == ' ' || char == '\t' => { - // If there are chars on the word stack, push that word - // onto the stanza - if !word.is_empty() { - stanza.push(word.iter().collect()); - word.clear(); - } - } - - // The character is any other utf8 glyph - Some(char) => { - // Add the character onto the current word stack - word.push(char); - } - - // Indicates the end of the list of characters - None => { - push!( - word, - stanza, - Verse::couplet(prev), - prev, - verses, - Meter::None - ); - - // 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, - }; - - // Increment the index - i += 1; - } - - // Return the (parsed) poem - Some(Poem::new(verses)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_parses_a_verse_with_no_meter() { - let poem = Poem::read("cargo build --release".to_string()); - assert!(poem.is_some()); - let poem = poem.unwrap(); - assert_eq!(poem.verses.first().unwrap().verb(), "cargo"); - } - - #[test] - fn it_parses_a_verse_with_the_couplet_meter() { - let poem = Poem::read("ls -la | lolcat".to_string()); - assert!(poem.is_some()); - let poem = poem.unwrap(); - assert_eq!(poem.verses.first().unwrap().verb(), "ls"); - assert_eq!(poem.verses.first().unwrap().meter, Meter::Couplet); - } - - #[test] - fn it_parses_a_verse_with_the_quiet_meter() { - let poem = Poem::read("sleep 20 &".to_string()); - assert!(poem.is_some()); - let poem = poem.unwrap(); - assert_eq!(poem.verses.first().unwrap().verb(), "sleep"); - assert_eq!(poem.verses.first().unwrap().meter, Meter::Quiet); - } - - #[test] - fn it_parses_a_verse_with_the_and_meter() { - let poem = Poem::read("sleep 2 && ls -la".to_string()); - assert!(poem.is_some()); - let poem = poem.unwrap(); - assert_eq!(poem.verses.first().unwrap().verb(), "sleep"); - assert_eq!(poem.verses.first().unwrap().meter, Meter::And); - } - - #[test] - fn it_parses_a_verse_with_the_string_meter() { - let poem = Poem::read("sleep 2; ls -la".to_string()); - assert!(poem.is_some()); - let poem = poem.unwrap(); - assert_eq!(poem.verses.first().unwrap().verb(), "sleep"); - assert_eq!(poem.verses.first().unwrap().meter, Meter::String); - } - - #[test] - fn it_parses_verse_with_the_read_meter() { - let poem = Poem::read("lolcat < src/main.rs".to_string()); - assert!(poem.is_some()); - let mut verses = poem.unwrap().verses.into_iter(); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "lolcat"); - assert_eq!(verse.meter, Meter::Read); - - let verse = verses.next().unwrap(); - assert_eq!(verse.stanza(), vec!["src/main.rs".to_string()]); - } - - #[test] - fn it_parses_verse_with_the_write_meter() { - let poem = Poem::read("cat src/main.rs > /dev/null".to_string()); - assert!(poem.is_some()); - let mut verses = poem.unwrap().verses.into_iter(); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "cat"); - assert_eq!(verse.clause(), vec!["src/main.rs".to_string()]); - assert_eq!(verse.meter, Meter::Write); - - let verse = verses.next().unwrap(); - assert_eq!(verse.stanza(), vec!["/dev/null".to_string()]); - } - - #[test] - fn it_parses_verse_with_the_addenum_meter() { - let poem = Poem::read("cat src/main.rs >> /dev/null".to_string()); - assert!(poem.is_some()); - let mut verses = poem.unwrap().verses.into_iter(); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "cat"); - assert_eq!(verse.clause(), vec!["src/main.rs".to_string()]); - assert_eq!(verse.meter, Meter::Addendum); - - let verse = verses.next().unwrap(); - assert_eq!(verse.stanza(), vec!["/dev/null".to_string()]); - } - - #[test] - fn it_throws_a_parse_error_if_no_files_are_specified_for_the_read_meter() { - let poem = Poem::read("lolcat <".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("lolcat <;".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("lolcat < && ls -la".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_throws_a_parse_error_if_no_files_are_specified_for_the_write_meter() { - let poem = Poem::read("cat src/main.rs >".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("cat src/main.rs >;".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("cat > && ls -la".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_throws_a_parse_error_if_no_files_are_specified_for_the_addendum_meter() { - let poem = Poem::read("cat src/main.rs >>".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("cat src/main.rs >>;".to_string()); - assert!(poem.is_none()); - let poem = Poem::read("cat >> && ls -la".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_parses_a_complex_verse_with_lots_of_different_meters() { - let poem = Poem::read("ls -la | lolcat && echo hello | lolcat && sleep 2 &".to_string()); - assert!(poem.is_some()); - let mut verses = poem.unwrap().verses.into_iter(); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "ls"); - assert_eq!(verse.clause(), vec!["-la".to_string()]); - assert_eq!(verse.meter, Meter::Couplet); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "lolcat"); - assert_eq!(verse.meter, Meter::And); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "echo"); - assert_eq!(verse.clause(), vec!["hello".to_string()]); - assert_eq!(verse.meter, Meter::Couplet); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "lolcat"); - assert_eq!(verse.meter, Meter::And); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "sleep"); - assert_eq!(verse.clause(), vec!["2".to_string()]); - assert_eq!(verse.meter, Meter::Quiet); - } - - #[test] - fn it_parses_the_string_meter_without_a_stanza() { - let poem = Poem::read(";;;;;;;".to_string()); - assert!(poem.is_some()); - } - - #[test] - fn it_errors_if_the_couplet_meter_is_used_without_a_stanza() { - let poem = Poem::read("|".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_errors_if_the_quiet_meter_is_used_without_a_stanza() { - let poem = Poem::read("&".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_errors_if_the_and_meter_is_used_without_a_stanza() { - let poem = Poem::read("&&".to_string()); - assert!(poem.is_none()); - } - - #[test] - fn it_parses_a_file() { - let file = r" - ps aux | lolcat - sleep 2 - "; - - let poem = Poem::read(file.to_string()); - assert!(poem.is_some()); - - let poem = poem.unwrap(); - assert_eq!(poem.verses.len(), 3); - - let mut verses = poem.verses.into_iter(); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "ps"); - assert_eq!(verse.clause(), vec!["aux".to_string()]); - assert_eq!(verse.meter, Meter::Couplet); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "lolcat"); - assert_eq!(verse.meter, Meter::String); - - let verse = verses.next().unwrap(); - assert_eq!(verse.verb(), "sleep"); - assert_eq!(verse.clause(), vec!["2".to_string()]); - assert_eq!(verse.meter, Meter::String); - } - - #[test] - fn it_parses_a_longer_file() { - let file = r" - ps aux | lolcat - sleep 2 - ps aux | lolcat - sleep 2 - - echo hello there - export PATH=$PATH:~/.local/bin - - ps aux | lolcat && lolcat src/main.rs - fortune | cowsay | lolcat - - wc -l src/**/*.rs | lolcat; ls -la | grep git - "; - - let poem = Poem::read(file.to_string()); - assert!(poem.is_some()); - - let poem = poem.unwrap(); - assert_eq!(poem.verses.len(), 18); - } -} |