diff options
-rw-r--r-- | src/main.rs | 29 | ||||
-rw-r--r-- | src/path.rs (renamed from src/recite/path.rs) | 0 | ||||
-rw-r--r-- | src/poem.rs | 284 | ||||
-rw-r--r-- | src/poem/elements.rs | 4 | ||||
-rw-r--r-- | src/poem/elements/rune.rs | 363 | ||||
-rw-r--r-- | src/poem/elements/stanza.rs | 10 | ||||
-rw-r--r-- | src/poem/elements/verse.rs | 159 | ||||
-rw-r--r-- | src/poem/elements/word.rs | 2 | ||||
-rw-r--r-- | src/poem/read.rs | 261 | ||||
-rw-r--r-- | src/poem/read/parse.rs | 68 | ||||
-rw-r--r-- | src/poem/recite.rs | 222 | ||||
-rw-r--r-- | src/poem/recite/ps.rs (renamed from src/recite/ps.rs) | 53 | ||||
-rw-r--r-- | src/recite.rs | 1043 | ||||
-rw-r--r-- | src/recite/parse.rs | 135 |
14 files changed, 1438 insertions, 1195 deletions
diff --git a/src/main.rs b/src/main.rs index 04d58eb..75c2d46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ -mod recite; -use recite::path::prefresh; -use recite::Poem; use std::io::{self, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; +mod path; +use path::prefresh; +mod poem; +use poem::{read::Readable, recite::Reciteable, Poem}; /// Starts the main shell loop /// @@ -57,15 +58,23 @@ fn repl(path: &Vec<&Path>, prompt: &str, at_prompt: &mut Arc<Mutex<bool>>) { // Not at the prompt *at_prompt.lock().unwrap() = false; - // Parse a poem + // Parse the poem let poem = Poem::read(poetry); - match poem { - Some(poem) => match poem.recite(path, &mut bins) { - Ok(_) => {} - Err(e) => eprintln!("dwvsh: {}", e.to_string().to_lowercase()), - }, - None => {} + let poem = match poem { + Ok(poem) => poem, + Err(e) => { + eprintln!("dwvsh: {}", e.to_string().to_lowercase()); + continue; + } }; + + // println!("{:?}", poem); + + // Recite the poem + match poem.recite(path, &mut bins, None) { + Ok(_) => {} + Err(e) => eprintln!("dwvsh: {}", e.to_string().to_lowercase()), + } } } diff --git a/src/recite/path.rs b/src/path.rs index 28eb45b..28eb45b 100644 --- a/src/recite/path.rs +++ b/src/path.rs diff --git a/src/poem.rs b/src/poem.rs new file mode 100644 index 0000000..c0e7d6b --- /dev/null +++ b/src/poem.rs @@ -0,0 +1,284 @@ +mod elements; +use elements::verse::Verse; +pub mod read; +pub mod recite; + +/// Parse and run shell commands or `dwvsh` files +/// +/// A [Poem] is the structure that contains a full shell command/program. It +/// may be composed of one or many [Verse]'s. +pub type Poem = Vec<Verse>; + +#[cfg(test)] +mod tests { + use super::elements::rune::Rune; + use super::read::Readable; + use super::*; + + #[test] + fn it_parses_a_verse_with_no_meter() { + let poem = Poem::read("cargo build --release".to_string()); + assert!(poem.is_ok()); + let poem = poem.unwrap(); + assert_eq!(poem.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_ok()); + let poem = poem.unwrap(); + assert_eq!(poem.first().unwrap().verb(), "ls"); + assert_eq!(poem.first().unwrap().meter, Rune::Couplet); + } + + #[test] + fn it_parses_a_verse_with_the_quiet_meter() { + let poem = Poem::read("sleep 20 &".to_string()); + assert!(poem.is_ok()); + let poem = poem.unwrap(); + assert_eq!(poem.first().unwrap().verb(), "sleep"); + assert_eq!(poem.first().unwrap().meter, Rune::Quiet); + } + + #[test] + fn it_parses_a_verse_with_the_and_meter() { + let poem = Poem::read("sleep 2 && ls -la".to_string()); + assert!(poem.is_ok()); + let poem = poem.unwrap(); + assert_eq!(poem.first().unwrap().verb(), "sleep"); + assert_eq!(poem.first().unwrap().meter, Rune::And); + } + + #[test] + fn it_parses_a_verse_with_the_continue_meter() { + let poem = Poem::read("sleep 2; ls -la".to_string()); + assert!(poem.is_ok()); + let poem = poem.unwrap(); + assert_eq!(poem.first().unwrap().verb(), "sleep"); + assert_eq!(poem.first().unwrap().meter, Rune::Continue); + } + + // #[test] + // fn it_parses_verse_with_the_read_meter() { + // let poem = Poem::read("lolcat < src/main.rs".to_string()); + // assert!(poem.is_ok()); + // let mut verses = poem.unwrap().into_iter(); + + // let verse = verses.next().unwrap(); + // assert_eq!(verse.verb(), "lolcat"); + // assert_eq!(verse.meter, Rune::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_ok()); + // let mut verses = poem.unwrap().into_iter(); + + // let verse = verses.next().unwrap(); + // assert_eq!(verse.verb(), "cat"); + // assert_eq!(verse.clause().unwrap(), vec!["src/main.rs".to_string()]); + // assert_eq!(verse.meter, Rune::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_ok()); + // let mut verses = poem.unwrap().into_iter(); + + // let verse = verses.next().unwrap(); + // assert_eq!(verse.verb(), "cat"); + // assert_eq!(verse.clause().unwrap(), vec!["src/main.rs".to_string()]); + // assert_eq!(verse.meter, Rune::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_err()); + let poem = Poem::read("lolcat <;".to_string()); + assert!(poem.is_err()); + let poem = Poem::read("lolcat < && ls -la".to_string()); + assert!(poem.is_err()); + } + + #[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_err()); + let poem = Poem::read("cat src/main.rs >;".to_string()); + assert!(poem.is_err()); + let poem = Poem::read("cat > && ls -la".to_string()); + assert!(poem.is_err()); + } + + #[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_err()); + let poem = Poem::read("cat src/main.rs >>;".to_string()); + assert!(poem.is_err()); + let poem = Poem::read("cat >> && ls -la".to_string()); + assert!(poem.is_err()); + } + + #[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_ok()); + let mut verses = poem.unwrap().into_iter(); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "ls"); + assert_eq!(verse.clause().unwrap(), vec!["-la".to_string()]); + assert_eq!(verse.meter, Rune::Couplet); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "lolcat"); + assert_eq!(verse.meter, Rune::And); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "echo"); + assert_eq!(verse.clause().unwrap(), vec!["hello".to_string()]); + assert_eq!(verse.meter, Rune::Couplet); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "lolcat"); + assert_eq!(verse.meter, Rune::And); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "sleep"); + assert_eq!(verse.clause().unwrap(), vec!["2".to_string()]); + assert_eq!(verse.meter, Rune::Quiet); + } + + #[test] + fn it_parses_the_continue_meter_without_a_stanza() { + let poem = Poem::read(";;;;;;;".to_string()); + assert!(poem.is_ok()); + } + + #[test] + fn it_errors_if_the_couplet_meter_is_used_without_a_stanza() { + let poem = Poem::read("|".to_string()); + assert!(poem.is_err()); + } + + #[test] + fn it_errors_if_the_quiet_meter_is_used_without_a_stanza() { + let poem = Poem::read("&".to_string()); + assert!(poem.is_err()); + } + + #[test] + fn it_errors_if_the_and_meter_is_used_without_a_stanza() { + let poem = Poem::read("&&".to_string()); + assert!(poem.is_err()); + } + + #[test] + fn it_parses_a_file() { + let file = r" + ps aux | lolcat + sleep 2 + "; + + let poem = Poem::read(file.to_string()); + assert!(poem.is_ok()); + + let poem = poem.unwrap(); + assert_eq!(poem.len(), 3); + + let mut verses = poem.into_iter(); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "ps"); + assert_eq!(verse.clause().unwrap(), vec!["aux".to_string()]); + assert_eq!(verse.meter, Rune::Couplet); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "lolcat"); + assert_eq!(verse.meter, Rune::Continue); + + let verse = verses.next().unwrap(); + assert_eq!(verse.verb(), "sleep"); + assert_eq!(verse.clause().unwrap(), vec!["2".to_string()]); + assert_eq!(verse.meter, Rune::Continue); + } + + #[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_ok()); + + let poem = poem.unwrap(); + assert_eq!(poem.len(), 18); + } + + #[test] + fn it_catches_parser_errors_related_to_invalid_use_of_special_runes() { + let poetry = "cat file.txt &&&".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + + let poetry = "cat file.txt&&|".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + + let poetry = "cat <".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + } + + #[test] + fn it_catches_parser_errors_related_to_strings() { + let poetry = "echo 'hello".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + + let poetry = "echo \"hello".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + + let poetry = "`true".to_string(); + assert_eq!(Poem::read(poetry).is_err(), true); + } + + #[test] + fn it_interprets_tilda_as_home() { + let poetry = "cd ~".to_string(); + let poem = Poem::read(poetry).unwrap(); + assert_eq!(poem[0].verb(), "cd"); + assert_eq!(poem[0].clause(), Some(vec!["/home/rory".to_string()])); + + let poetry = "cd ~/Code/dwarvish".to_string(); + let poem = Poem::read(poetry).unwrap(); + assert_eq!(poem[0].verb(), "cd"); + assert_eq!( + poem[0].clause(), + Some(vec!["/home/rory/Code/dwarvish".to_string()]) + ); + assert_eq!(poem[0].meter, Rune::None); + } +} diff --git a/src/poem/elements.rs b/src/poem/elements.rs new file mode 100644 index 0000000..6cb4c7c --- /dev/null +++ b/src/poem/elements.rs @@ -0,0 +1,4 @@ +pub mod rune; +pub mod stanza; +pub mod verse; +pub mod word; diff --git a/src/poem/elements/rune.rs b/src/poem/elements/rune.rs new file mode 100644 index 0000000..fc2b27a --- /dev/null +++ b/src/poem/elements/rune.rs @@ -0,0 +1,363 @@ +use super::verse::Verse; +use crate::iobtask; +use crate::{btask, ctask, task}; +use core::fmt; +use libc::waitpid; +use libc::WNOHANG; +use std::fs::OpenOptions; +use std::io::{self, Read, Write}; +use std::os::unix::process::CommandExt; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; + +/// Describes one or two characters from the input +/// +/// [Rune]s are a way to mark special characters from the input string (i.e. +/// poetry). Some [Rune]s are special--as they denote the end of a [Verse]-- +/// and are refered to as a Meter. For instance, `Addendum`, `Couplet`, +/// `Quiet`, and `And`, are all meters. Meters also determine how the +/// [Stanza][super::stanza::Stanza] should be interpreted. For instance, a +/// [Stanza][super::stanza::Stanza] that is piped needs to have +/// its `STDOUT` captured (rather than printing out to the terminal), and +/// subsequently sent to the next [Verse] in the [Poem][super::super::Poem]. +/// +/// # Values +/// * `None` - A shell command with no additional actions (the end of a poem) +/// * `Pause` - The space character, to dilineate words (` `) +/// * `Path` - The forward slash character, to dilineate paths (`/`) +/// * `Env` - Indicates an environment variable (`$`) +/// * `String` - Interpret all character as one large +/// [Word][super::word::Word] (`'` or `"`) +/// * `Poem` - A subcommand to run first (`\``) +/// * `Read` - Read files into STDIN (`<`) +/// * `Write` - Write STDOUT to a file (`>`) +/// * `Addendum` - Append STDOUT to a file (`>>`) +/// * `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 (`&&`) +/// * `Continue` - String commands together on a single line (`;`) +/// * `Home` - Interpret `~` as `$HOME` +/// * `Else` - Any other character +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Rune { + None, // No meter (the end of a poem) + Pause, // A space + Path, // A forward slash + String, // Interpret the following as one large [Word] + Poem, // Run a sub-poem before the main one + Read, // Read files into STDIN + Write, // Send STDOUT to a file + Addendum, // Append STDOUT to a file + 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 + Continue, // Run the next command, even if this doesn't succeed + Home, // Interpret '~' as $HOME + Else, // Any other character +} + +impl fmt::Display for Rune { + /// Determine how to print out a [Rune] + /// + /// Each [Rune]'s symbol corresponds to its input. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let rune = match self { + Rune::None => "", + Rune::Pause => " ", + Rune::Path => "/", + Rune::String => "\"", + Rune::Poem => "`", + Rune::Read => "<", + Rune::Write => ">", + Rune::Addendum => ">>", + Rune::Couplet => "|", + Rune::Quiet => "&", + Rune::And => "&&", + Rune::Continue => ";", + Rune::Home => "~", + Rune::Else => "_", + }; + + write!(f, "{}", rune) + } +} + +impl Rune { + // /// Check if a character is a special [Rune] + // pub fn special(rune: char) -> bool { + // match rune { + // ' ' | '/' | '$' | '\'' | '"' | '`' | '<' | '>' | '|' | '&' | ';' | '~' => true, + // _ => false, + // } + // } + + /// Recite a verse with [Rune::None] + /// + /// Call this function on a [Verse] with a meter of type [Rune::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 + pub 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 [Rune::Couplet] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Couplet]. + /// 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 + pub 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 [Rune::Quiet] + /// + /// Call this function on a [Verse] with a meter of type [Rune::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 + pub 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.join(" ").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 [Rune::incant_none] + pub fn incant_and(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + Rune::incant_none(verse, out) + } + + /// Alias to [Rune::incant_none] + pub fn incant_continue(verse: &Verse, out: &mut String) -> Result<i32, io::Error> { + Rune::incant_none(verse, out) + } + + /// Recite a verse with [Rune::Read] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Read]. + /// This reads the specified files into `out`, then makes a call to + /// [Rune::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` + pub fn incant_read( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // 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.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_<meter> + match verse.meter { + Rune::None => Rune::incant_none(&verse, out), + Rune::Couplet => Rune::incant_couplet(&verse, out), + Rune::Quiet => Rune::incant_quiet(&verse, out, pids), + Rune::And => Rune::incant_and(&verse, out), + Rune::Continue => Rune::incant_continue(&verse, out), + _ => unreachable!(), + } + } + + /// Recite a verse with [Rune::Write] + /// + /// Call this function on a [Verse] with a meter of type [Rune::Write]. + /// This writes the output of the verse into the specified files, after + /// making a call to [Rune::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` + pub fn incant_write( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // Alias incant_<meter> + // let status = Rune::incant_couplet(&verse, out)?; + let status = match verse.meter { + Rune::None => Rune::incant_none(&verse, out)?, + Rune::Couplet => Rune::incant_couplet(&verse, out)?, + Rune::Quiet => Rune::incant_quiet_io(&verse, out, pids)?, + Rune::And => Rune::incant_and(&verse, out)?, + Rune::Continue => Rune::incant_continue(&verse, out)?, + _ => unreachable!(), + }; + + // Write output to each file specified in the next verse + for path in paths.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 [Rune::Addendum] + /// + /// Same as [Rune::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` + pub fn incant_addendum( + verse: &mut Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + // Split the verse from the paths + let paths = verse.split("<"); + + // Alias incant_<meter> + // let status = Rune::incant_couplet(&verse, out)?; + let status = match verse.meter { + Rune::None => Rune::incant_none(&verse, out)?, + Rune::Couplet => Rune::incant_couplet(&verse, out)?, + Rune::Quiet => Rune::incant_quiet_io(&verse, out, pids)?, + Rune::And => Rune::incant_and(&verse, out)?, + Rune::Continue => Rune::incant_continue(&verse, out)?, + _ => unreachable!(), + }; + + // Write output to each file specified in the next verse + for path in paths.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) + } + + pub fn incant_quiet_io( + verse: &Verse, + out: &mut String, + pids: &mut Arc<Mutex<Vec<i32>>>, + ) -> Result<i32, io::Error> { + let child = iobtask!(verse, out); + println!("[&] {}", child.id()); + + pids.lock().unwrap().push(child.id() as i32); + let stanza = verse.stanza.join(" ").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) + } +} diff --git a/src/poem/elements/stanza.rs b/src/poem/elements/stanza.rs new file mode 100644 index 0000000..d58d080 --- /dev/null +++ b/src/poem/elements/stanza.rs @@ -0,0 +1,10 @@ +/// The actionable part of a [Verse][super::verse::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. +/// +/// The [Stanza] is just stored as a [Vec] of [String]s, where the verb is the +/// first entry in the vector (i.e. `stanza[0]`) and the clause the the +/// remainder of the vector (i.e. `stanza[1..]`). +pub type Stanza = Vec<String>; diff --git a/src/poem/elements/verse.rs b/src/poem/elements/verse.rs new file mode 100644 index 0000000..e857676 --- /dev/null +++ b/src/poem/elements/verse.rs @@ -0,0 +1,159 @@ +use super::rune::Rune; +use super::stanza::Stanza; +use super::word::Word; +use crate::poem::Poem; +use std::path::Path; + +/// A [Stanza] and it's [meter](Rune) +/// +/// In addition to a [Stanza] and a [meter](Rune), this also holds a [bool] +/// value called `couplet`, indicating that it needs to accept input on `STDIN` +/// from the previous [Verse]. +#[derive(Debug, Clone)] +pub struct Verse { + pub stanza: Stanza, + pub couplet: bool, + pub io: Rune, + pub poems: Vec<Poem>, + pub meter: Rune, +} + +impl Verse { + /// Create a new [Verse] + /// + /// Returns a new [Verse], with an empty [Stanza], a meter of [Rune::None], + /// and `couplet` set to `false`. + pub fn new() -> Self { + Verse { + stanza: Stanza::new(), + couplet: false, + io: Rune::None, + poems: Vec::new(), + meter: Rune::None, + } + } + + /// Get the [Verse]'s verb + /// + /// Return the program to be forked + pub fn verb(&self) -> String { + self.stanza[0].clone() + } + + /// Get the [Verse]'s clause + /// + /// Return program arguments, if they exist + pub fn clause(&self) -> Option<Vec<String>> { + match self.stanza.len() { + 0 => None, + 1 => None, + _ => Some(self.stanza[1..].to_vec()), + } + } + + /// Alias to [Verse].stanza.push() + pub fn push(&mut self, word: String) { + self.stanza.push(word); + } + + /// Alias to [Verse].stanza.is_empty() + pub fn is_empty(&self) -> bool { + self.stanza.is_empty() + } + + /// Alias to [Verse].stanza.clear() + pub fn clear(&mut self) { + self.stanza.clear(); + } + + /// Check if the [Verse] contains any internal poems + pub fn poems(&self) -> bool { + if self.poems.len() > 0 { + return true; + } + false + } + + /// Push a word to the [Verse]'s [Stanza] + /// + /// Push a word to the [Stanza] after performing a few extra checks, such + /// as whether or not the word is empty, or if the word should be + /// interpreted as an environment variable. + pub fn add(&mut self, word: &mut Word) { + if !word.is_empty() { + // Push the word, and clear the stack + self.push(word.iter().collect()); + word.clear(); + } + } + + /// Split a [Verse] into two different [Verse]s + /// + /// This is useful for [Rune::Read], [Rune::Write], and [Rune::Addendum]. + pub fn split(&mut self, c: &str) -> Vec<String> { + for (i, s) in self.stanza.iter().enumerate() { + if *s == c { + let split = self.stanza.split_off(i); + return split[1..].to_vec(); + } + } + 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 seeif 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 + /// ``` + pub 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 + } +} diff --git a/src/poem/elements/word.rs b/src/poem/elements/word.rs new file mode 100644 index 0000000..dd088e0 --- /dev/null +++ b/src/poem/elements/word.rs @@ -0,0 +1,2 @@ +/// A (typically) space dilineated piece of a [Stanza][super::stanza::Stanza] +pub type Word = Vec<char>; diff --git a/src/poem/read.rs b/src/poem/read.rs new file mode 100644 index 0000000..01ddfc3 --- /dev/null +++ b/src/poem/read.rs @@ -0,0 +1,261 @@ +use super::{ + elements::{rune::Rune, verse::Verse, word::Word}, + Poem, +}; +use core::fmt; +mod parse; +use crate::{next, poem, string}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Mishap { + ParseMishap(usize, usize, char), + IOMishap(usize, usize, char), + PartialMishap(usize, usize, char), +} + +impl fmt::Display for Mishap { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message = match self { + Mishap::ParseMishap(j, i, c) => { + format!("parse error on line {} pos {} near '{}'", j, i, c) + } + Mishap::IOMishap(j, i, c) => { + format!( + "must provide file for io operation on line {} pos {} near '{}'", + j, i, c + ) + } + Mishap::PartialMishap(j, i, c) => { + format!( + "partial string or action on line {} pos {} near '{}'", + j, i, c + ) + } + }; + + write!(f, "{}", message) + } +} + +/// A [Poem] can add more [Verse]s to itself +trait Appendable { + type Type; + fn add(&mut self, verse: &mut Self::Type, meter: Rune, last: Rune); +} + +impl Appendable for Poem { + type Type = Verse; + + /// Push a [Verse] to the [Poem] + /// + /// Push a [Verse] to the [Poem] after checking that the [Verse] is not + /// empty. Also sets the meter of the [Verse]. + fn add(&mut self, verse: &mut Self::Type, last: Rune, meter: Rune) { + if !verse.is_empty() { + verse.meter = meter; + if last == Rune::Couplet || meter == Rune::Couplet { + verse.couplet = true; + } + self.push(verse.clone()); + verse.clear(); + } + } +} + +/// A [Poem] can parse poetry +pub trait Readable { + fn read(poetry: String) -> Result<Poem, Mishap>; +} + +impl Readable for Poem { + /// Parse a [Poem] from a raw [String] input + /// + /// Takes a shell command/program or file and converts it to a + /// machine-runnable [Poem]. If there is a parse error, [Poem::read] may + /// return a [Mishap]. See [Poem::recite][super::recite] for how each + /// [Verse] in a [Poem] is called. + fn read(poetry: String) -> Result<Poem, Mishap> { + // Get all the characters in the input string as an iterator + let mut chars = poetry.chars().into_iter(); + + // Create a stack to store words + let mut word: Word = Word::new(); + + // Create a stack to store the current verse + let mut verse: Verse = Verse::new(); + + // Create a vector to return + let mut poem: Self = Poem::new(); + + // Keep track of the last rune + let mut last = Rune::None; + + // Keep track of the line + let mut j = 0; + + // Keep track of the column + let mut i = 0; + + // Loop through every char in the iterator + loop { + // Get the next character, and unwrap it + let c = chars.next(); + let c = match c { + Some(c) => c, + None => { + // Check for IO parse errors + if last == Rune::Read || last == Rune::Write || last == Rune::Addendum { + return Err(Mishap::IOMishap(j, i, ' ')); + } + + // If c is none, it indicates the end of a poem, so wrap up and + // then break from the loop + verse.add(&mut word); + + // Throw an error if the verse is empty + if verse.is_empty() && (last == Rune::Couplet || last == Rune::And) { + return Err(Mishap::ParseMishap(j, i, ' ')); + } + + // Push the verse and break + poem.add(&mut verse, last, Rune::None); + break; + } + }; + + // Determine the meter based on the character + let rune = match c { + ' ' => Rune::Pause, + '/' => Rune::Path, + '\'' | '"' => Rune::String, + '`' => Rune::Poem, + '<' => { + verse.couplet = true; + Rune::Read + } + '>' => next!(chars, i, Rune::Write, Rune::Addendum, '>'), + '|' => Rune::Couplet, + '&' => next!(chars, i, Rune::Quiet, Rune::And, '&'), + ';' => Rune::Continue, + '\n' => { + j += 1; + i = 0; + Rune::Continue + } + '~' => Rune::Home, + _ => Rune::Else, + }; + + // Some error checking, based on the last character + match rune { + Rune::Couplet + | Rune::Quiet + | Rune::And + | Rune::Read + | Rune::Write + | Rune::Addendum => { + if (last == Rune::Couplet + || last == Rune::Quiet + || last == Rune::And + || last == Rune::Read + || last == Rune::Write + || last == Rune::Addendum) + || verse.is_empty() + { + return Err(Mishap::ParseMishap(j, i, c)); + } + } + + Rune::Continue => { + if last == Rune::Read || last == Rune::Write || last == Rune::Addendum { + return Err(Mishap::ParseMishap(j, i, c)); + } + } + + _ => { + if (last == Rune::Read || last == Rune::Write || last == Rune::Addendum) + && rune == Rune::None + && rune == Rune::Read + && rune == Rune::Write + && rune == Rune::Addendum + && rune == Rune::Couplet + && rune == Rune::Quiet + && rune == Rune::And + && rune == Rune::Continue + { + return Err(Mishap::IOMishap(j, i, c)); + } + } + }; + + // Do some action, based on the rune + match rune { + // Indicates the end of a word (space dilineated) + Rune::Pause => { + verse.add(&mut word); + } + + // Indicates a string (' or ") + Rune::String => { + string!(chars, j, i, c, word); + } + + // Indicates a sub-poem + Rune::Poem => { + poem!(chars, j, i, c, verse, word); + // let sp = Poem::read(word.iter().collect()); + // let sp = match sp { + // Ok(sp) => sp, + // Err(e) => return Err(e), + // }; + // verse.poems.push(sp); + // word.push('\x0b'); + // verse.push(format!("\x0b{}", verse.poems.len() - 1)); + // word.clear(); + } + + // Indicates a file operation (<, >, or >>) + Rune::Read | Rune::Write | Rune::Addendum => { + verse.add(&mut word); + word.push('<'); + verse.add(&mut word); + verse.io = rune; + } + + // These meters indicate the end of a verse + Rune::Couplet | Rune::Quiet | Rune::And | Rune::Continue => { + verse.add(&mut word); + poem.add(&mut verse, last, rune); + } + + // Interpret ~ as $HOME + Rune::Home => { + let mut chars = env!("HOME").chars().collect(); + word.append(&mut chars); + } + + // Any other char i.e. Meter::Else + _ => { + word.push(c); + } + } + + // Set the last meter + if rune != Rune::Pause { + last = rune; + } + // last = match poem.last() { + // Some(last) => last.meter, + // None => Rune::None, + // }; + + // Increment i, but don't drift over newlines + if c != '\n' { + i += 1; + } + } + + // Return the poem + Ok(poem) + } +} diff --git a/src/poem/read/parse.rs b/src/poem/read/parse.rs new file mode 100644 index 0000000..c4d59e6 --- /dev/null +++ b/src/poem/read/parse.rs @@ -0,0 +1,68 @@ +/// Look ahead one character in the input +/// +/// May need to look ahead one character in the input string to determine the +/// proper rune. For instance `&`, vs `&&`. +#[macro_export] +macro_rules! next { + ($chars:expr, $i:expr, $otherwise:expr, $rune:expr, $ahead:expr) => { + match $chars.clone().peekable().peek() { + Some(c) if *c == $ahead => { + $chars.next(); + $i += 1; + $rune + } + Some(_) => $otherwise, + None => $otherwise, + } + }; +} + +/// Keep pushing to the [Word][super::super::elements::word::Word] stack +/// +/// If a [Rune::String][super::super::elements::rune::Rune] character is found, +/// stop interpreting special characters, and push all characters to the +/// [Word][super::super::elements::word::Word] stack, until the corresponding +/// [Rune::String][super::super::elements::rune::Rune] character is found. +#[macro_export] +macro_rules! string { + ($chars:expr, $j:expr, $i:expr, $c:expr, $word:expr) => { + let token = $c; + loop { + match $chars.next() { + None => return Err(Mishap::PartialMishap($j, $i, $c)), + Some(c) if c == token => break, + Some(c) => { + $word.push(c); + $i += 1; + } + } + } + continue; + }; +} + +/// Same as the [string!] macro, but don't `continue` +#[macro_export] +macro_rules! poem { + ($chars:expr, $j:expr, $i:expr, $c:expr, $verse:expr, $word:expr) => { + let token = $c; + let mut poetry = Word::new(); + loop { + match $chars.next() { + None => return Err(Mishap::PartialMishap($j, $i, $c)), + Some(c) if c == token => break, + Some(c) => { + poetry.push(c); + $i += 1; + } + } + } + let sp = Poem::read(poetry.iter().collect()); + let sp = match sp { + Ok(sp) => sp, + Err(e) => return Err(e), + }; + $verse.poems.push(sp); + $word.push('\x0b'); + }; +} diff --git a/src/poem/recite.rs b/src/poem/recite.rs new file mode 100644 index 0000000..ba739c2 --- /dev/null +++ b/src/poem/recite.rs @@ -0,0 +1,222 @@ +mod ps; +use super::Poem; +use crate::path::prefresh; +use crate::poem::elements::rune::Rune; +use std::env; +use std::process::exit; +use std::{ + io, + path::Path, + sync::{Arc, Mutex}, +}; + +pub trait Reciteable { + fn recite( + &self, + path: &Vec<&Path>, + bins: &mut Vec<String>, + stdout: Option<bool>, + ) -> Result<String, io::Error>; +} + +impl Reciteable for Poem { + fn recite( + &self, + path: &Vec<&Path>, + bins: &mut Vec<String>, + stdout: Option<bool>, + ) -> Result<String, io::Error> { + // Should we print to stdout or always capture it + let stdout = stdout.unwrap_or(true); + + // Variable for storing the output of a piped verse + let mut out: String = String::new(); + + // Keep track of pids for background processes + let mut pids: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new())); + + // Loop through each verse in the poem + for verse in self.iter() { + // Verse may need to be mutable + let mut verse = verse.clone(); + + // Check for environment variables + for word in verse.stanza.iter_mut() { + while word.contains("$") { + let mut name = vec![]; + let mut idx = word.chars().position(|c| c == '$').unwrap() + 1; + let bytes = word.as_bytes(); + while idx < word.len() { + let c = bytes[idx] as char; + if !c.is_alphanumeric() { + break; + } + name.push(c); + idx += 1; + } + let name: String = format!("${}", name.iter().collect::<String>()); + let envar = name[1..].to_string(); + let envar = match env::var(envar) { + Ok(envar) => envar.to_string(), + Err(_) => "".to_string(), + }; + *word = word.replace(name.as_str(), envar.as_str()); + } + } + + // Run interal poems + let v = verse.clone(); + let mut new_stanza = None; + if verse.poems() { + // Collect all the words that have vertical tabs + let mut wordp_indicies = vec![]; + let wordps = verse + .stanza + .iter_mut() + .enumerate() + .filter(|(_, w)| w.contains("\x0b")) + .map(|(i, w)| { + wordp_indicies.push(i + 1); + w + }) + .collect::<Vec<&mut String>>(); + + // Loop through each word and replace with the output of the poem + let mut poems = verse.poems.iter(); + let mut j = 0; + for wordp in wordps { + let times = wordp + .chars() + .filter(|c| c == &'\x0b') + .collect::<String>() + .len(); + for _ in 0..times { + let poem = match poems.next() { + Some(poem) => poem, + None => break, // TODO: Return an error + }; + let out = poem.recite(path, bins, Some(false))?; + if out.contains("\n") { + let mut out = out.split("\n"); + let next = out.next().unwrap_or("").trim(); + *wordp = wordp.replacen("\x0b", next, 1).to_string(); + let (_, right) = v.stanza.split_at(wordp_indicies[j]); + let mut left = vec![]; + let mut right = right.to_vec(); + loop { + let next = match out.next() { + Some(next) => next, + None => break, + } + .to_string(); + left.push(next); + } + left.append(&mut right); + new_stanza = Some(left.clone()); + } else { + *wordp = wordp.replacen("\x0b", out.as_str(), 1).to_string(); + } + } + j += 1; + } + + // // Get indices of words in the verse that begin with a vertical tab + // let mut indicies = vec![]; + // for (i, word) in verse.stanza.iter().enumerate() { + // if word.starts_with("\x0b") { + // indicies.push(i); + // } + // } + + // // Try to recite the internal poem, and update the parent poem + // for (i, poem) in verse.poems.iter().enumerate() { + // let out = poem.recite(path, bins, Some(false))?; + // verse.stanza[indicies[i]] = out; + // } + } + + match new_stanza { + Some(stanza) => { + let mut stanza = stanza.clone(); + verse.stanza.append(&mut stanza); + } + None => {} + } + + // Check if the 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 = match verse.clause() { + Some(path) => path[0].to_string(), + None => env!("HOME").to_string(), + }; + + match std::env::set_current_dir(&path) { + Ok(_) => continue, + Err(e) => { + eprintln!( + "cd: unable to change into {}: {}", + path, + e.to_string().to_lowercase() + ); + 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) { + eprintln!("dwvsh: {}: command not found", verse.verb()); + + if verse.meter != Rune::And { + continue; + } + } + } + + // Incant the verse, based on its meter + let status = if stdout { + match verse.io { + Rune::Read => Rune::incant_read(&mut verse, &mut out, &mut pids)?, + Rune::Write => Rune::incant_write(&mut verse, &mut out, &mut pids)?, + Rune::Addendum => Rune::incant_addendum(&mut verse, &mut out, &mut pids)?, + _ => match verse.meter { + Rune::None => Rune::incant_none(&verse, &mut out)?, + Rune::Couplet => Rune::incant_couplet(&verse, &mut out)?, + Rune::Quiet => Rune::incant_quiet(&verse, &mut out, &mut pids)?, + Rune::And => Rune::incant_and(&verse, &mut out)?, + Rune::Continue => Rune::incant_continue(&verse, &mut out)?, + _ => unreachable!(), + }, + } + } else { + match verse.io { + Rune::Read => Rune::incant_read(&mut verse, &mut out, &mut pids)?, + Rune::Write => Rune::incant_write(&mut verse, &mut out, &mut pids)?, + Rune::Addendum => Rune::incant_addendum(&mut verse, &mut out, &mut pids)?, + _ => Rune::incant_couplet(&verse, &mut out)?, + } + }; + + // Break from the loop if the meter is not [Rune::Continue], and + // if the status is not 0 + // For [Rune::Quiet], [Rune::incant_quiet] will always return 0 + if verse.meter != Rune::Continue && status != 0 { + break; + } + } + + // If we've successfully exited the loop, then all verses were properly + // recited + Ok(out.trim().to_string()) + } +} diff --git a/src/recite/ps.rs b/src/poem/recite/ps.rs index 0738057..61ed66d 100644 --- a/src/recite/ps.rs +++ b/src/poem/recite/ps.rs @@ -12,7 +12,7 @@ macro_rules! task { ($verse:expr, $out:expr) => { if $verse.couplet { let mut child = Command::new($verse.verb()) - .args($verse.clause()) + .args($verse.clause().unwrap_or(vec![])) .stdin(Stdio::piped()) .spawn()?; @@ -22,7 +22,9 @@ macro_rules! task { child } else { - Command::new($verse.verb()).args($verse.clause()).spawn()? + Command::new($verse.verb()) + .args($verse.clause().unwrap_or(vec![])) + .spawn()? } }; } @@ -42,7 +44,7 @@ macro_rules! ctask { ($verse:expr, $out:expr) => { if $verse.couplet { let mut child = Command::new($verse.verb()) - .args($verse.clause()) + .args($verse.clause().unwrap_or(vec![])) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; @@ -54,14 +56,13 @@ macro_rules! ctask { child } else { Command::new($verse.verb()) - .args($verse.clause()) + .args($verse.clause().unwrap_or(vec![])) .stdout(Stdio::piped()) .spawn()? } }; } -#[macro_export] /// Fork into a background process from a Verse /// /// Figures out whether or not the given Verse is a couplet. If it is, fork @@ -71,12 +72,49 @@ macro_rules! ctask { /// # Arguments /// * `$verse: &Verse` - The verse to fork into /// * `$out: &mut String` - If the $verse is a couplet, the contents of STDOUT from the last verse +#[macro_export] macro_rules! btask { ($verse:expr, $out:expr) => { if $verse.couplet { let mut child = Command::new($verse.verb()) - .args($verse.clause()) + .args($verse.clause().unwrap_or(vec![])) + .stdin(Stdio::piped()) + .process_group(0) + .spawn()?; + + let stdin = child.stdin.as_mut().ok_or(io::ErrorKind::BrokenPipe)?; + stdin.write_all(&$out.as_bytes())?; + $out.clear(); + + child + } else { + Command::new($verse.verb()) + .args($verse.clause().unwrap_or(vec![])) + .process_group(0) + .spawn()? + } + }; +} + +/// Fork into a background process from a Verse, and capture STDOUT +/// +/// Figures out whether or not the given Verse is a couplet. If it is, fork +/// into a backgournd process, and pipe the contents of out `out` into STDIN. +/// If not, then simply fork into the background process. This captures the +/// output of STDOUT, in order to redirect it to a file when the program +/// finishes running. +/// +/// # Arguments +/// * `$verse: &Verse` - The verse to fork into +/// * `$out: &mut String` - If the $verse is a couplet, the contents of STDOUT from the last verse +#[macro_export] +macro_rules! iobtask { + ($verse:expr, $out:expr) => { + if $verse.couplet { + let mut child = Command::new($verse.verb()) + .args($verse.clause().unwrap_or(vec![])) .stdin(Stdio::piped()) + .stdout(Stdio::piped()) .process_group(0) .spawn()?; @@ -87,7 +125,8 @@ macro_rules! btask { child } else { Command::new($verse.verb()) - .args($verse.clause()) + .args($verse.clause().unwrap_or(vec![])) + .stdout(Stdio::piped()) .process_group(0) .spawn()? } 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); - } -} diff --git a/src/recite/parse.rs b/src/recite/parse.rs deleted file mode 100644 index 63b16ff..0000000 --- a/src/recite/parse.rs +++ /dev/null @@ -1,135 +0,0 @@ -/// Add a Verse to a Poem -/// -/// Takes the current word stack, and pushes it onto the stanza. Then, takes -/// the stanza, meter, and some metadata details to create a Verse. That Verse -/// then gets added to the Poem. -/// -/// # Arguments -/// * `$word` - The word stack, used to hold chars dilineated via whitespace -/// * `$stanza` - The stanza stack, used to hold words when they are popped off -/// the word stack -/// * `$cprev` - Indicates this is the second half of a couplet (normally this -/// is just Verse::couplet(prev), but we may need to set it in -/// case of Meter::Read), since this basically just tells the -/// shell to use `$out` in the `task!` macro -/// * `$prev` - The previous verse (or none if there is no previous verse) -/// * `$verses` - The list of verses that make up the poem -/// * `$meter` - The meter corresponding to the verse -#[macro_export] -macro_rules! push { - ($word:expr, $stanza:expr, $cprev:expr, $prev:expr, $verses:expr, $meter:expr) => { - // 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, - $cprev, - Verse::rw($prev), - )); - } - - // Clear the stacks - $stanza.clear(); - $word.clear(); - }; -} - -/// Add a Verse to a Poem, but allow looking ahead by one char -/// -/// This works the exact same as [[push]], except that it doesn't take -/// `$cprev` (since there is no need to set it manually right now), and it -/// takes `$ahead`, which is the next character to look for in the pattern, -/// along with `$aheadm`, which is the corresponding Meter if that `$ahead` is -/// found. -/// -/// # Examples -/// ``` -/// push1!(word, stanza, prev, verses, Meter::Quiet, '&', Meter::And); -/// push1!(word, stanza, prev, verses, Meter::Write, '>', Meter::Addendum); -/// ``` -#[macro_export] -macro_rules! push1 { - ($word:expr, $stanza:expr, $chars:expr, $prev:expr, $verses: expr, $meter:expr, $ahead:expr, $aheadm:expr) => { - // 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 == &$ahead => { - // 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()), - $aheadm, - 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, - 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, - Verse::couplet($prev), - Verse::rw($prev), - )); - } - } - - // Clear the stacks - $stanza.clear(); - $word.clear(); - }; -} |