diff options
author | Rory Dudley | 2024-03-02 20:40:32 -0700 |
---|---|---|
committer | Rory Dudley | 2024-03-02 20:40:32 -0700 |
commit | c87fa3f288447d6913ffa561849b90bf8e24da54 (patch) | |
tree | 64420ab0111b079155b4caebe9465a4bf1160485 | |
parent | 718f45492a4b2c31a67458c13c4cd4b3268703bc (diff) | |
download | dwarvish-c87fa3f288447d6913ffa561849b90bf8e24da54.tar.gz |
Add file redirection capabilities
Added the following file redirection capabilies:
- '<': Read input into STDIN
- '>': Write STDOUT to file
- '>>': Append STDOUT to file
If no files are specified, this counts as a parser error, and so no code
will be executed, even when used in combination with the string (';')
meter. Currently, there is no way to redirect STDERR.
-rw-r--r-- | src/recite.rs | 351 |
1 files changed, 337 insertions, 14 deletions
diff --git a/src/recite.rs b/src/recite.rs index 1aa1a62..d4128b7 100644 --- a/src/recite.rs +++ b/src/recite.rs @@ -4,7 +4,8 @@ use crate::{btask, ctask, task}; use core::fmt; use libc::{waitpid, WNOHANG}; use path::prefresh; -use std::io::{self, Write}; +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}; @@ -23,13 +24,19 @@ use std::sync::{Arc, Mutex}; /// * `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 (`;`) -#[derive(Debug, PartialEq, Eq)] +/// * `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 + 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 { @@ -43,6 +50,9 @@ impl fmt::Display for Meter { Meter::Quiet => "&", Meter::And => "&&", Meter::String => ";", + Meter::Read => "<", + Meter::Write => ">", + Meter::Addendum => ">>", }; write!(f, "{}", meter) @@ -160,6 +170,109 @@ impl Meter { 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> { + // // If there are no more verses, throw an error + // if i + 1 >= verses.len() { + // return Err(std::io::Error::new( + // io::ErrorKind::NotFound, + // "read file(s) not specified", + // )); + // } + + // 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> { + // If there are no more verses, throw an error + // if i + 1 >= verses.len() { + // return Err(std::io::Error::new( + // io::ErrorKind::NotFound, + // "write file(s) not specified", + // )); + // } + + // 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())?; + } + + // 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> { + // If there are no more verses, throw an error + // if i + 1 >= verses.len() { + // return Err(std::io::Error::new( + // io::ErrorKind::NotFound, + // "write file(s) not specified", + // )); + // } + + // 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())?; + } + + // Return the exit status + Ok(status) + } } /// Holds a program to be called @@ -290,6 +403,7 @@ struct Verse { stanza: Stanza, meter: Meter, couplet: bool, + rw: bool, } impl fmt::Display for Verse { @@ -311,11 +425,12 @@ impl 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) -> Verse { + fn new(stanza: Stanza, meter: Meter, couplet: bool, rw: bool) -> Verse { Verse { stanza, meter, couplet, + rw, } } @@ -334,12 +449,36 @@ impl Verse { 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 | Meter::Quiet | Meter::And | Meter::String => true, - Meter::None => false, + 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, } @@ -349,7 +488,13 @@ impl Verse { fn cadence(verse: Option<&Verse>) -> bool { match verse { Some(verse) => match verse.meter { - Meter::Couplet | Meter::Quiet | Meter::And | Meter::String => true, + Meter::Couplet + | Meter::Quiet + | Meter::And + | Meter::String + | Meter::Read + | Meter::Write + | Meter::Addendum => true, Meter::None => false, }, None => false, @@ -405,7 +550,13 @@ impl Poem { let mut pids: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new())); // Loop through each verse in the poem - for verse in self.verses.iter() { + 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); @@ -441,18 +592,66 @@ impl Poem { } } + let mut meter = verse.meter; + // Incant the verse, based on its meter - let status = match verse.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 verse.meter != Meter::String && status != 0 { + if meter != Meter::String && status != 0 { break; } } @@ -525,6 +724,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::Couplet, Verse::couplet(prev), + Verse::rw(prev), )); // Clear the stacks @@ -554,6 +754,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::And, Verse::couplet(prev), + Verse::rw(prev), )); } @@ -564,6 +765,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::Quiet, Verse::couplet(prev), + Verse::rw(prev), )); } @@ -575,6 +777,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::Quiet, Verse::couplet(prev), + Verse::rw(prev), )); // We can break out of the loop here, since it's @@ -596,12 +799,26 @@ impl Poem { stanza.push(word.iter().collect()); } + // Check if the last verse was a meter of Read, Write, or + // Addendum, and throw an error if it is + if Verse::rw(prev) && stanza.is_empty() { + let rw = match prev.unwrap().meter { + Meter::Read => "read", + Meter::Write => "write", + Meter::Addendum => "append", + _ => "", + }; + eprintln!("dwvsh: parse error: no {} file(s) specified", rw); + return None; + } + // A meter indicates the end of a verse if !stanza.is_empty() { verses.push(Verse::new( Stanza::new(stanza.clone()), Meter::String, Verse::couplet(prev), + Verse::rw(prev), )); } @@ -610,6 +827,97 @@ impl Poem { word.clear(); } + // The character represents the Read Meter + Some(meter) if meter == '<' => { + // If there are chars on the word stack, push that word + // onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Read, + true, + Verse::rw(prev), + )); + + // Clear the stacks + stanza.clear(); + word.clear(); + } + + // The character represents the Write Meter + Some(meter) if meter == '>' => { + // If there are chars on the word stack, push that word + // onto the stanza + if !word.is_empty() { + stanza.push(word.iter().collect()); + } + + // Check if the last verse was a meter of Read, Write, or + // Addendum, and throw an error if it is + if Verse::rw(prev) && stanza.is_empty() { + let rw = match prev.unwrap().meter { + Meter::Read => "read", + Meter::Write => "write", + Meter::Addendum => "append", + _ => "", + }; + eprintln!("dwvsh: parse error: no {} file(s) specified", rw); + return None; + } + + // Need to peek at the next character, since '>' can mean + // Meter::Write, or '>>' can mean Meter::Addendum + match chars.clone().peekable().peek() { + // Indicated Meter::Addendum + Some(c) if c == &'>' => { + // Pop the next character from the input string + // (since we know what it is) + chars.next(); + + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Addendum, + Verse::couplet(prev), + Verse::rw(prev), + )); + } + + // Indicates Meter::Write + Some(_) => { + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Write, + Verse::couplet(prev), + Verse::rw(prev), + )); + } + + // Indicated the end of the input + None => { + // A meter indicates the end of a verse + verses.push(Verse::new( + Stanza::new(stanza.clone()), + Meter::Write, + Verse::couplet(prev), + Verse::rw(prev), + )); + + // We can break out of the loop here, since it's + // the end of the raw input + } + } + + // Clear the stacks + stanza.clear(); + word.clear(); + } + // The character is a newline (may happen if parsing from a file) Some(char) if char == '\n' => { // If there are chars on the word stack, push that word @@ -624,6 +932,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::String, Verse::couplet(prev), + Verse::rw(prev), )); } @@ -655,6 +964,19 @@ impl Poem { stanza.push(word.iter().collect()); } + // Check if the last verse was a meter of Read, Write, or + // Addendum, and throw an error if it is + if Verse::rw(prev) && stanza.is_empty() { + let rw = match prev.unwrap().meter { + Meter::Read => "read", + Meter::Write => "write", + Meter::Addendum => "append", + _ => "", + }; + eprintln!("dwvsh: parse error: no {} file(s) specified", rw); + return None; + } + // Only push the stanza into a verse if it contains // any words if !stanza.is_empty() { @@ -662,6 +984,7 @@ impl Poem { Stanza::new(stanza.clone()), Meter::None, Verse::couplet(prev), + Verse::rw(prev), )); } |