pub mod path; mod ps; use crate::{btask, ctask, 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 { 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 { 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>>` - 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>>, ) -> Result { 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 { Meter::incant_none(verse, out) } /// Alias to [Meter::incant_none] fn incant_string(verse: &Verse, out: &mut String) -> Result { 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 { // 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 { // 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 { // 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; /// 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`. The first element of /// the vector becomes the [Verb], while the remainder of the vector /// becomes the [Clause]. /// /// # Arguments /// `stanza: Vec` - 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>(); /// let stanza = Stanza::new(command); /// println!("{}", stanza.verb); /// println!("{:?}", stanza.clause); /// /// ``` fn new(stanza: Vec) -> 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>(); /// /// let command_success = vec!["cargo", "build", "--release"] /// .into_iter() /// .map(String::from) /// .collect>(); /// /// let command_fail = vec!["make", "-j8"] /// .into_iter() /// .map(String::from) /// .collect>(); /// /// 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) -> 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) -> 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 { self.stanza.clause.clone() } /// Return the entire [stanza][Stanza] fn stanza(&self) -> Vec { 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, } impl Poem { /// Create a new [Poem] /// /// Returns a new [Poem] built from a list of [verse's][Verse]. fn new(verses: Vec) -> 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) -> Result<(), io::Error> { // Variable for storing the output of a piped verse let mut out: String = String::new(); let mut pids: Arc>> = 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 { // 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 = Vec::new(); // Accumulate verses let mut stanza: Vec = Vec::new(); // Stack for each stanza let mut word: Vec = 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 == '|' => { // 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, Verse::couplet(prev), Verse::rw(prev), )); // 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, Verse::couplet(prev), Verse::rw(prev), )); } // Indicates Meter::Quiet Some(_) => { // A meter indicates the end of a verse verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::Quiet, Verse::couplet(prev), Verse::rw(prev), )); } // 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, Verse::couplet(prev), Verse::rw(prev), )); // 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()); } // Check if the last verse was a meter of Read, Write, or // Addendum, and throw an error if it is if Verse::rw(prev) && stanza.is_empty() { let rw = match prev.unwrap().meter { Meter::Read => "read", Meter::Write => "write", Meter::Addendum => "append", _ => "", }; eprintln!("dwvsh: parse error: no {} file(s) specified", rw); return None; } // A meter indicates the end of a verse if !stanza.is_empty() { verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::String, Verse::couplet(prev), Verse::rw(prev), )); } // Clear the stacks stanza.clear(); word.clear(); } // The character represents the Read 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::Read, true, Verse::rw(prev), )); // Clear the stacks stanza.clear(); word.clear(); } // The character represents the Write 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()); } // Check if the last verse was a meter of Read, Write, or // Addendum, and throw an error if it is if Verse::rw(prev) && stanza.is_empty() { let rw = match prev.unwrap().meter { Meter::Read => "read", Meter::Write => "write", Meter::Addendum => "append", _ => "", }; eprintln!("dwvsh: parse error: no {} file(s) specified", rw); return None; } // Need to peek at the next character, since '>' can mean // Meter::Write, or '>>' can mean Meter::Addendum match chars.clone().peekable().peek() { // Indicated Meter::Addendum 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::Addendum, Verse::couplet(prev), Verse::rw(prev), )); } // Indicates Meter::Write Some(_) => { // A meter indicates the end of a verse verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::Write, Verse::couplet(prev), Verse::rw(prev), )); } // Indicated the end of the input None => { // A meter indicates the end of a verse verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::Write, Verse::couplet(prev), Verse::rw(prev), )); // We can break out of the loop here, since it's // the end of the raw input } } // Clear the stacks stanza.clear(); word.clear(); } // The character is a newline (may happen if parsing from a file) Some(char) if char == '\n' => { // If there are chars on the word stack, push that word // onto the stanza if !word.is_empty() { stanza.push(word.iter().collect()); } // A newline indicates the end of a verse if !stanza.is_empty() { verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::String, Verse::couplet(prev), Verse::rw(prev), )); } // Clear the stacks stanza.clear(); word.clear(); } // 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 => { // Always push the last word onto the stanza if !word.is_empty() { stanza.push(word.iter().collect()); } // Check if the last verse was a meter of Read, Write, or // Addendum, and throw an error if it is if Verse::rw(prev) && stanza.is_empty() { let rw = match prev.unwrap().meter { Meter::Read => "read", Meter::Write => "write", Meter::Addendum => "append", _ => "", }; eprintln!("dwvsh: parse error: no {} file(s) specified", rw); return None; } // 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, Verse::couplet(prev), Verse::rw(prev), )); } // 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_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); } }