diff options
author | Rory Dudley | 2024-03-23 02:45:54 -0600 |
---|---|---|
committer | Rory Dudley | 2024-03-23 02:45:54 -0600 |
commit | 5a7718698373d07a29fffcb792acdb81aa7712d7 (patch) | |
tree | e0147ced4a484e02295cd6a6f0f6dd2250d381c8 | |
parent | 37e1ae98dc9309715e9415962f21484a807d2c56 (diff) | |
download | dwarvish-5a7718698373d07a29fffcb792acdb81aa7712d7.tar.gz |
read() and recite() overhaul
Rebuilt the LR parser (i.e. read()) from the ground up. This required
that some changes be made to recite(), in order to accomodate the new
data structures. These data structures were each split out into their
own file, in order to make working with each component a bit easier.
In addition to reworking the parts of the parser already present, some
new features were also added, such as:
- Support for strings (' and ")
- Support for environment variables ($)
- Support for interpreting tild as $HOME (~)
- Support for sub-reading and sub-reciting (`)
Notes
Notes:
This is a huge commit that changes almost the entire program (main.rs is
still the same, except for imports). Ideally, huge sweeping changes like
this should not occur on the codebase, but since this is still
pre-alpha, I guess this is acceptable. This is far from the end of
patch set, however, as there is quite a lot of cleanup that needs to be
done. For instance, checking for internal poems and environment
variables should get split out to their own functions/macros. There is
also some defunct code (that's commented out), that is unlikely to be
useful in the future.
-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(); - }; -} |