From b0c437afceb3465eecbf658077bfc1b74d611c1e Mon Sep 17 00:00:00 2001
From: Hunter Goldstein <hunter.d.goldstein@gmail.com>
Date: Thu, 13 Jul 2017 14:13:28 -0400
Subject: [PATCH] Implement Herestrings (#405)

---
 examples/herestring.ion | 14 ++++++++
 examples/herestring.out |  3 ++
 src/parser/peg.rs       | 40 +++++++++++++++++-----
 src/parser/pipelines.rs | 75 ++++++++++++++++++++++++++++-------------
 src/shell/pipe.rs       | 68 ++++++++++++++++++++++++++++++-------
 5 files changed, 156 insertions(+), 44 deletions(-)
 create mode 100644 examples/herestring.ion
 create mode 100644 examples/herestring.out

diff --git a/examples/herestring.ion b/examples/herestring.ion
new file mode 100644
index 00000000..55b988e2
--- /dev/null
+++ b/examples/herestring.ion
@@ -0,0 +1,14 @@
+echo $(tr '[a-z]' '[A-Z]' <<< foo)
+
+let output = [foo bar baz bing]
+
+fn find elem array
+  if grep -q $elem <<< $array
+    echo "true"
+  else
+    echo "false"
+  end
+end
+
+find "bar" "@output"
+find "zar" "@output"
diff --git a/examples/herestring.out b/examples/herestring.out
new file mode 100644
index 00000000..46bae8fc
--- /dev/null
+++ b/examples/herestring.out
@@ -0,0 +1,3 @@
+FOO
+true
+false
diff --git a/src/parser/peg.rs b/src/parser/peg.rs
index 9f606e18..ce1db239 100644
--- a/src/parser/peg.rs
+++ b/src/parser/peg.rs
@@ -18,15 +18,24 @@ pub struct Redirection {
     pub append: bool
 }
 
+/// Represents input that a process could initially receive from `stdin`
+#[derive(Debug, PartialEq, Clone)]
+pub enum Input {
+    /// A file; the contents of said file will be written to the `stdin` of a process
+    File(String),
+    /// A string literal that is written to the `stdin` of a process
+    HereString(String),
+}
+
 #[derive(Debug, PartialEq, Clone)]
 pub struct Pipeline {
     pub jobs:   Vec<Job>,
     pub stdout: Option<Redirection>,
-    pub stdin:  Option<Redirection>,
+    pub stdin:  Option<Input>,
 }
 
 impl Pipeline {
-    pub fn new(jobs: Vec<Job>, stdin: Option<Redirection>, stdout: Option<Redirection>) -> Self {
+    pub fn new(jobs: Vec<Job>, stdin: Option<Input>, stdout: Option<Redirection>) -> Self {
         Pipeline { jobs, stdin, stdout }
     }
 
@@ -36,9 +45,17 @@ impl Pipeline {
             job.expand(&expanders);
         }
 
-        if let Some(stdin) = self.stdin.iter_mut().next() {
-            stdin.file = expand_string(stdin.file.as_str(), &expanders, false).join(" ");
-        }
+        let stdin = match self.stdin {
+            Some(Input::File(ref s)) => {
+                Some(Input::File(expand_string(s, &expanders, false).join(" ")))
+            },
+            Some(Input::HereString(ref s)) => {
+                Some(Input::HereString(expand_string(s, &expanders, true).join(" ")))
+            },
+            None => None
+        };
+
+        self.stdin = stdin;
 
         if let Some(stdout) = self.stdout.iter_mut().next() {
             stdout.file = expand_string(stdout.file.as_str(), &expanders, false).join(" ");
@@ -62,9 +79,16 @@ impl fmt::Display for Pipeline {
                 JobKind::Pipe(RedirectFrom::Both) => tokens.push("&|".into()),
             }
         }
-        if let Some(ref infile) = self.stdin {
-            tokens.push("<".into());
-            tokens.push(infile.file.clone());
+        match self.stdin {
+            None => (),
+            Some(Input::File(ref file)) => {
+                tokens.push("<".into());
+                tokens.push(file.clone());
+            },
+            Some(Input::HereString(ref string)) => {
+                tokens.push("<<<".into());
+                tokens.push(string.clone());
+            },
         }
         if let Some(ref outfile) = self.stdout {
             match outfile.from {
diff --git a/src/parser/pipelines.rs b/src/parser/pipelines.rs
index e6d444ab..fc2fe2a1 100644
--- a/src/parser/pipelines.rs
+++ b/src/parser/pipelines.rs
@@ -9,7 +9,7 @@
 use std::collections::HashSet;
 use std::iter::Peekable;
 
-use parser::peg::{Pipeline, Redirection, RedirectFrom};
+use parser::peg::{Pipeline, Input, Redirection, RedirectFrom};
 use shell::{Job, JobKind};
 use types::*;
 
@@ -165,7 +165,7 @@ impl<'a> Collector<'a> {
         let mut bytes = self.data.bytes().enumerate().peekable();
         let mut args = Array::new();
         let mut jobs: Vec<Job> = Vec::new();
-        let mut infile: Option<Redirection> = None;
+        let mut input: Option<Input> = None;
         let mut outfile: Option<Redirection> = None;
 
         /// Attempt to create a new job given a list of collected arguments
@@ -270,16 +270,22 @@ impl<'a> Collector<'a> {
                 },
                 b'<' => {
                     bytes.next();
-                    if let Some(file) = self.arg(&mut bytes)? {
-                        infile = Some(Redirection {
-                            from: RedirectFrom::Stdout,
-                            file: file.into(),
-                            append: false,
-                        });
+                    if Some(b'<') == self.peek(i + 1) && Some(b'<') == self.peek(i + 2) {
+                        // If the next two characters are arrows, then interpret
+                        // the next argument as a herestring
+                        bytes.next();
+                        bytes.next();
+                        if let Some(cmd) = self.arg(&mut bytes)? {
+                            input = Some(Input::HereString(cmd.into()));
+                        } else {
+                            return Err("expected string argument after '<<<'");
+                        }
+                    } else if let Some(file) = self.arg(&mut bytes)? {
+                        // Otherwise interpret it as stdin redirection
+                        input = Some(Input::File(file.into()));
                     } else {
                         return Err("expected file argument after redirection for input");
                     }
-
                 }
                 // Skip over whitespace between jobs
                 b' ' | b'\t' => {
@@ -294,7 +300,7 @@ impl<'a> Collector<'a> {
             jobs.push(Job::new(args, JobKind::Last));
         }
 
-        Ok(Pipeline::new(jobs, infile, outfile))
+        Ok(Pipeline::new(jobs, input, outfile))
     }
 
 }
@@ -302,7 +308,7 @@ impl<'a> Collector<'a> {
 #[cfg(test)]
 mod tests {
     use shell::flow_control::Statement;
-    use parser::peg::{parse, Pipeline, RedirectFrom, Redirection};
+    use parser::peg::{parse, Input, Pipeline, RedirectFrom, Redirection};
     use shell::{Job, JobKind};
     use types::Array;
 
@@ -631,7 +637,7 @@ mod tests {
             assert_eq!("echo", &pipeline.clone().jobs[1].args[0]);
             assert_eq!("hello", &pipeline.clone().jobs[1].args[1]);
             assert_eq!("cat", &pipeline.clone().jobs[2].args[0]);
-            assert_eq!("stuff", &pipeline.clone().stdin.unwrap().file);
+            assert_eq!(Some(Input::File("stuff".into())), pipeline.stdin);
             assert_eq!("other", &pipeline.clone().stdout.unwrap().file);
             assert!(!pipeline.clone().stdout.unwrap().append);
             assert_eq!(input.to_owned(), pipeline.to_string());
@@ -644,7 +650,7 @@ mod tests {
     fn pipeline_with_redirection_append() {
         if let Statement::Pipeline(pipeline) = parse("cat | echo hello | cat < stuff >> other") {
         assert_eq!(3, pipeline.jobs.len());
-        assert_eq!("stuff", &pipeline.clone().stdin.unwrap().file);
+        assert_eq!(Some(Input::File("stuff".into())), pipeline.stdin);
         assert_eq!("other", &pipeline.clone().stdout.unwrap().file);
         assert!(pipeline.clone().stdout.unwrap().append);
         } else {
@@ -661,11 +667,7 @@ mod tests {
                 Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)),
                 Job::new(array!["cat"], JobKind::Last)
             ],
-            stdin: Some(Redirection {
-                from: RedirectFrom::Stdout,
-                file: "stuff".into(),
-                append: false
-            }),
+            stdin: Some(Input::File("stuff".into())),
             stdout: Some(Redirection {
                 from: RedirectFrom::Stderr,
                 file: "other".into(),
@@ -684,11 +686,7 @@ mod tests {
                 Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)),
                 Job::new(array!["cat"], JobKind::Last)
             ],
-            stdin: Some(Redirection {
-                from: RedirectFrom::Stdout,
-                file: "stuff".into(),
-                append: false
-            }),
+            stdin: Some(Input::File("stuff".into())),
             stdout: Some(Redirection {
                 from: RedirectFrom::Both,
                 file: "other".into(),
@@ -702,7 +700,7 @@ mod tests {
     fn pipeline_with_redirection_reverse_order() {
         if let Statement::Pipeline(pipeline) = parse("cat | echo hello | cat > stuff < other") {
             assert_eq!(3, pipeline.jobs.len());
-            assert_eq!("other", &pipeline.clone().stdin.unwrap().file);
+            assert_eq!(Some(Input::File("other".into())), pipeline.stdin);
             assert_eq!("stuff", &pipeline.clone().stdout.unwrap().file);
         } else {
             assert!(false);
@@ -731,6 +729,35 @@ mod tests {
         }
     }
 
+    #[test]
+    fn herestring() {
+        let input = "calc <<< $(cat math.txt)";
+        let expected = Pipeline {
+            jobs: vec![Job::new(array!["calc"], JobKind::Last)],
+            stdin: Some(Input::HereString("$(cat math.txt)".into())),
+            stdout: None,
+        };
+        assert_eq!(Statement::Pipeline(expected), parse(input));
+    }
+
+    #[test]
+    fn piped_herestring() {
+        let input = "cat | tr 'o' 'x' <<< $VAR > out.log";
+        let expected = Pipeline {
+            jobs: vec![
+                Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)),
+                Job::new(array!["tr", "'o'", "'x'"], JobKind::Last)
+            ],
+            stdin: Some(Input::HereString("$VAR".into())),
+            stdout: Some(Redirection {
+                from: RedirectFrom::Stdout,
+                file: "out.log".into(),
+                append: false
+            })
+        };
+        assert_eq!(Statement::Pipeline(expected), parse(input));
+    }
+
     #[test]
     fn awk_tests() {
         if let Statement::Pipeline(pipeline) = parse("awk -v x=$x '{ if (1) print $1 }' myfile") {
diff --git a/src/shell/pipe.rs b/src/shell/pipe.rs
index 99c8579c..281f72c1 100644
--- a/src/shell/pipe.rs
+++ b/src/shell/pipe.rs
@@ -11,7 +11,7 @@ use super::job_control::JobControl;
 use super::{JobKind, Shell};
 use super::status::*;
 use super::signals::{self, SignalHandler};
-use parser::peg::{Pipeline, RedirectFrom};
+use parser::peg::{Pipeline, Input, RedirectFrom};
 use self::crossplat::*;
 
 /// The `crossplat` module contains components that are meant to be abstracted across
@@ -21,7 +21,7 @@ pub mod crossplat {
     use nix::{fcntl, unistd};
     use parser::peg::{RedirectFrom};
     use std::fs::File;
-    use std::io::Error;
+    use std::io::{Write, Error};
     use std::os::unix::io::{IntoRawFd, FromRawFd};
     use std::process::{Stdio, Command};
 
@@ -34,6 +34,21 @@ pub mod crossplat {
         unistd::getpid() as u32
     }
 
+    /// Create an instance of Stdio from a byte slice that will echo the
+    /// contents of the slice when read. This can be called with owned or
+    /// borrowed strings
+    pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<Stdio, Error> {
+        let (reader, writer) = unistd::pipe2(fcntl::O_CLOEXEC)?;
+        let mut infile = File::from_raw_fd(writer);
+        // Write the contents; make sure to use write_all so that we block until
+        // the entire string is written
+        infile.write_all(input.as_ref())?;
+        infile.flush()?;
+        // `infile` currently owns the writer end RawFd. If we just return the reader end
+        // and let `infile` go out of scope, it will be closed, sending EOF to the reader!
+        Ok(Stdio::from_raw_fd(reader))
+    }
+
     /// Set up pipes such that the relevant output of parent is sent to the stdin of child.
     /// The content that is sent depends on `mode`
     pub unsafe fn create_pipe (
@@ -68,7 +83,7 @@ pub mod crossplat {
 pub mod crossplat {
     use parser::peg::{RedirectFrom};
     use std::fs::File;
-    use std::io;
+    use std::io::{self, Write};
     use std::os::unix::io::{IntoRawFd, FromRawFd};
     use std::process::{Stdio, Command};
     use syscall;
@@ -95,6 +110,19 @@ pub mod crossplat {
         fn from(data: syscall::Error) -> Error { Error::Sys(data) }
     }
 
+    pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<Stdio, Error> {
+        let mut fds: [usize; 2] = [0; 2];
+        syscall::call::pipe2(&mut fds, syscall::flag::O_CLOEXEC)?;
+        let (reader, writer) = (fds[0], fds[1]);
+        let infile = File::from_raw_fd(writer);
+        // Write the contents; make sure to use write_all so that we block until
+        // the entire string is written
+        infile.write_all(input.as_ref())?;
+        // `infile` currently owns the writer end RawFd. If we just return the reader end
+        // and let `infile` go out of scope, it will be closed, sending EOF to the reader!
+        Ok(Stdio::from_raw_fd(reader))
+    }
+
     /// Set up pipes such that the relevant output of parent is sent to the stdin of child.
     /// The content that is sent depends on `mode`
     pub unsafe fn create_pipe (
@@ -159,15 +187,31 @@ impl<'a> PipelineExecution for Shell<'a> {
             .drain(..).map(|mut job| {
                 (job.build_command(), job.kind)
             }).collect();
-
-        if let Some(ref stdin) = pipeline.stdin {
-            if let Some(command) = piped_commands.first_mut() {
-                match File::open(&stdin.file) {
-                    Ok(file) => unsafe { command.0.stdin(Stdio::from_raw_fd(file.into_raw_fd())); },
-                    Err(err) => {
-                        let stderr = io::stderr();
-                        let mut stderr = stderr.lock();
-                        let _ = writeln!(stderr, "ion: failed to redirect stdin into {}: {}", stdin.file, err);
+        match pipeline.stdin {
+            None => (),
+            Some(Input::File(ref filename)) => {
+                if let Some(command) = piped_commands.first_mut() {
+                    match File::open(filename) {
+                        Ok(file) => unsafe {
+                            command.0.stdin(Stdio::from_raw_fd(file.into_raw_fd()));
+                        },
+                        Err(e) => {
+                            eprintln!("ion: failed to redirect '{}' into stdin: {}", filename, e);
+                        }
+                    }
+                }
+            },
+            Some(Input::HereString(ref mut string)) => {
+                if let Some(command) = piped_commands.first_mut() {
+                    if !string.ends_with('\n') { string.push('\n'); }
+                    match unsafe { crossplat::stdin_of(&string) } {
+                        Ok(stdio) => {
+                            command.0.stdin(stdio);
+                        },
+                        Err(e) => {
+                            eprintln!("ion: failed to redirect herestring '{}' into stdin: {}",
+                                      string, e);
+                        }
                     }
                 }
             }
-- 
GitLab