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