summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRory Dudley2024-03-02 20:40:32 -0700
committerRory Dudley2024-03-02 20:40:32 -0700
commitc87fa3f288447d6913ffa561849b90bf8e24da54 (patch)
tree64420ab0111b079155b4caebe9465a4bf1160485
parent718f45492a4b2c31a67458c13c4cd4b3268703bc (diff)
downloaddwarvish-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.rs351
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),
));
}