summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main.rs29
-rw-r--r--src/path.rs (renamed from src/recite/path.rs)0
-rw-r--r--src/poem.rs284
-rw-r--r--src/poem/elements.rs4
-rw-r--r--src/poem/elements/rune.rs363
-rw-r--r--src/poem/elements/stanza.rs10
-rw-r--r--src/poem/elements/verse.rs159
-rw-r--r--src/poem/elements/word.rs2
-rw-r--r--src/poem/read.rs261
-rw-r--r--src/poem/read/parse.rs68
-rw-r--r--src/poem/recite.rs222
-rw-r--r--src/poem/recite/ps.rs (renamed from src/recite/ps.rs)53
-rw-r--r--src/recite.rs1043
-rw-r--r--src/recite/parse.rs135
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();
- };
-}