summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2022-01-07 23:42:32 -0500
committerJesse Luehrs <doy@tozt.net>2022-01-08 00:00:57 -0500
commit9dd1d992bd2344d8824e69d8f47c9009a6caf021 (patch)
treea2b3682c41a182bbb3b031e5c458ff93c460e53f
parenta2d265ce727caf4e8cd4bad034ef8ae830346fc3 (diff)
downloadnbsh-9dd1d992bd2344d8824e69d8f47c9009a6caf021.tar.gz
nbsh-9dd1d992bd2344d8824e69d8f47c9009a6caf021.zip
large refactor
-rw-r--r--src/parse.rs335
-rw-r--r--src/parse/ast.rs588
-rw-r--r--src/pipeline/builtins/command.rs16
-rw-r--r--src/pipeline/builtins/mod.rs6
-rw-r--r--src/pipeline/command.rs26
-rw-r--r--src/pipeline/mod.rs6
-rw-r--r--src/shell/history/mod.rs7
-rw-r--r--src/shell/mod.rs4
-rw-r--r--src/test_parse.rs285
9 files changed, 687 insertions, 586 deletions
diff --git a/src/parse.rs b/src/parse.rs
index 1dfb4f9..f147892 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -1,10 +1,65 @@
-use pest::Parser as _;
+pub mod ast;
-#[derive(pest_derive::Parser)]
-#[grammar = "shell.pest"]
-struct Shell;
+#[derive(Debug)]
+pub struct Commands {
+ pipelines: Vec<Pipeline>,
+}
+
+impl Commands {
+ pub fn pipelines(&self) -> &[Pipeline] {
+ &self.pipelines
+ }
+}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug)]
+pub struct Pipeline {
+ exes: Vec<Exe>,
+ input_string: String,
+}
+
+impl Pipeline {
+ pub fn into_exes(self) -> impl Iterator<Item = Exe> {
+ self.exes.into_iter()
+ }
+
+ pub fn input_string(&self) -> &str {
+ &self.input_string
+ }
+}
+
+#[derive(Debug)]
+pub struct Exe {
+ exe: std::path::PathBuf,
+ args: Vec<String>,
+ redirects: Vec<Redirect>,
+}
+
+impl Exe {
+ pub fn exe(&self) -> &std::path::Path {
+ &self.exe
+ }
+
+ pub fn args(&self) -> &[String] {
+ &self.args
+ }
+
+ pub fn redirects(&self) -> &[Redirect] {
+ &self.redirects
+ }
+
+ pub fn shift(&mut self) {
+ self.exe = std::path::PathBuf::from(self.args.remove(0));
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Redirect {
+ pub from: std::os::unix::io::RawFd,
+ pub to: RedirectTarget,
+ pub dir: Direction,
+}
+
+#[derive(Debug, Clone)]
pub enum RedirectTarget {
Fd(std::os::unix::io::RawFd),
File(std::path::PathBuf),
@@ -25,12 +80,12 @@ impl Direction {
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
Ok(match self {
- crate::parse::Direction::In => nix::fcntl::open(
+ Self::In => nix::fcntl::open(
path,
OFlag::O_NOCTTY | OFlag::O_RDONLY,
Mode::empty(),
)?,
- crate::parse::Direction::Out => nix::fcntl::open(
+ Self::Out => nix::fcntl::open(
path,
OFlag::O_CREAT
| OFlag::O_NOCTTY
@@ -43,7 +98,7 @@ impl Direction {
| Mode::S_IROTH
| Mode::S_IWOTH,
)?,
- crate::parse::Direction::Append => nix::fcntl::open(
+ Self::Append => nix::fcntl::open(
path,
OFlag::O_APPEND
| OFlag::O_CREAT
@@ -60,266 +115,6 @@ impl Direction {
}
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Redirect {
- pub from: std::os::unix::io::RawFd,
- pub to: RedirectTarget,
- pub dir: Direction,
-}
-
-impl Redirect {
- fn parse(prefix: &str, to: &str) -> Self {
- let (from, dir) = if let Some(from) = prefix.strip_suffix(">>") {
- (from, Direction::Append)
- } else if let Some(from) = prefix.strip_suffix('>') {
- (from, Direction::Out)
- } else if let Some(from) = prefix.strip_suffix('<') {
- (from, Direction::In)
- } else {
- unreachable!()
- };
- let from = if from.is_empty() {
- match dir {
- Direction::In => 0,
- Direction::Out | Direction::Append => 1,
- }
- } else {
- from.parse().unwrap()
- };
- let to = to.strip_prefix('&').map_or_else(
- || RedirectTarget::File(to.into()),
- |fd| RedirectTarget::Fd(fd.parse().unwrap()),
- );
- Self { from, to, dir }
- }
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct Word {
- word: String,
- interpolate: bool,
- quoted: bool,
-}
-
-impl Word {
- fn parse(s: &str, interpolate: bool, quoted: bool) -> Self {
- let mut word_str = s.to_string();
- if interpolate {
- word_str = strip_escape(&word_str);
- } else {
- word_str = strip_basic_escape(&word_str);
- }
- Self {
- word: word_str,
- interpolate,
- quoted,
- }
- }
-}
-
-enum WordOrRedirect {
- Word(Word),
- Redirect(Redirect),
-}
-
-impl WordOrRedirect {
- fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
- assert!(matches!(pair.as_rule(), Rule::word));
- let mut inner = pair.into_inner();
- let mut word = inner.next().unwrap();
- let mut prefix = None;
- if matches!(word.as_rule(), Rule::redir_prefix) {
- prefix = Some(word.as_str().trim().to_string());
- word = inner.next().unwrap();
- }
- assert!(matches!(
- word.as_rule(),
- Rule::bareword | Rule::single_string | Rule::double_string
- ));
- let word = Word::parse(
- word.as_str(),
- matches!(word.as_rule(), Rule::bareword | Rule::double_string),
- matches!(
- word.as_rule(),
- Rule::single_string | Rule::double_string
- ),
- );
- if let Some(prefix) = prefix {
- Self::Redirect(Redirect::parse(&prefix, &word.word))
- } else {
- Self::Word(word)
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct Exe {
- exe: Word,
- args: Vec<Word>,
- redirects: Vec<Redirect>,
-}
-
-impl Exe {
- fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
- assert!(matches!(pair.as_rule(), Rule::exe));
- let mut iter = pair.into_inner();
- let exe = match WordOrRedirect::build_ast(iter.next().unwrap()) {
- WordOrRedirect::Word(word) => word,
- WordOrRedirect::Redirect(_) => todo!(),
- };
- let (args, redirects): (_, Vec<_>) = iter
- .map(WordOrRedirect::build_ast)
- .partition(|word| matches!(word, WordOrRedirect::Word(_)));
- let args = args
- .into_iter()
- .map(|word| match word {
- WordOrRedirect::Word(word) => word,
- WordOrRedirect::Redirect(_) => unreachable!(),
- })
- .collect();
- let redirects = redirects
- .into_iter()
- .map(|word| match word {
- WordOrRedirect::Word(_) => unreachable!(),
- WordOrRedirect::Redirect(redirect) => redirect,
- })
- .collect();
- Self {
- exe,
- args,
- redirects,
- }
- }
-
- pub fn exe(&self) -> &str {
- &self.exe.word
- }
-
- pub fn args(&self) -> impl Iterator<Item = &str> {
- self.args.iter().map(|arg| arg.word.as_ref())
- }
-
- pub fn redirects(&self) -> &[Redirect] {
- &self.redirects
- }
-
- pub fn shift(&mut self) {
- self.exe = self.args.remove(0);
- }
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct Pipeline {
- exes: Vec<Exe>,
- input_string: String,
-}
-
-impl Pipeline {
- pub fn parse(pipeline: &str) -> Result<Self, Error> {
- Ok(Self::build_ast(
- Shell::parse(Rule::pipeline, pipeline)
- .map_err(|e| Error::new(pipeline, anyhow::anyhow!(e)))?
- .next()
- .unwrap(),
- ))
- }
-
- pub fn into_exes(self) -> impl Iterator<Item = Exe> {
- self.exes.into_iter()
- }
-
- pub fn input_string(&self) -> &str {
- &self.input_string
- }
-
- fn build_ast(pipeline: pest::iterators::Pair<Rule>) -> Self {
- assert!(matches!(pipeline.as_rule(), Rule::pipeline));
- let input_string = pipeline.as_str().to_string();
- Self {
- exes: pipeline.into_inner().map(Exe::build_ast).collect(),
- input_string,
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct Commands {
- pipelines: Vec<Pipeline>,
- input_string: String,
-}
-
-impl Commands {
- pub fn parse(full_cmd: &str) -> Result<Self, Error> {
- Ok(Self::build_ast(
- Shell::parse(Rule::line, full_cmd)
- .map_err(|e| Error::new(full_cmd, anyhow::anyhow!(e)))?
- .next()
- .unwrap()
- .into_inner()
- .next()
- .unwrap(),
- ))
- }
-
- pub fn pipelines(&self) -> &[Pipeline] {
- &self.pipelines
- }
-
- pub fn input_string(&self) -> &str {
- &self.input_string
- }
-
- fn build_ast(commands: pest::iterators::Pair<Rule>) -> Self {
- assert!(matches!(commands.as_rule(), Rule::commands));
- let input_string = commands.as_str().to_string();
- Self {
- pipelines: commands
- .into_inner()
- .map(Pipeline::build_ast)
- .collect(),
- input_string,
- }
- }
-}
-
-fn strip_escape(s: &str) -> String {
- let mut new = String::new();
- let mut escape = false;
- for c in s.chars() {
- if escape {
- new.push(c);
- escape = false;
- } else {
- match c {
- '\\' => escape = true,
- _ => new.push(c),
- }
- }
- }
- new
-}
-
-fn strip_basic_escape(s: &str) -> String {
- let mut new = String::new();
- let mut escape = false;
- for c in s.chars() {
- if escape {
- match c {
- '\\' | '\'' => {}
- _ => new.push('\\'),
- }
- new.push(c);
- escape = false;
- } else {
- match c {
- '\\' => escape = true,
- _ => new.push(c),
- }
- }
- }
- new
-}
-
#[derive(Debug)]
pub struct Error {
input: String,
@@ -350,7 +145,3 @@ impl std::error::Error for Error {
Some(&*self.e)
}
}
-
-#[cfg(test)]
-#[path = "test_parse.rs"]
-mod test;
diff --git a/src/parse/ast.rs b/src/parse/ast.rs
new file mode 100644
index 0000000..28593bb
--- /dev/null
+++ b/src/parse/ast.rs
@@ -0,0 +1,588 @@
+use crate::prelude::*;
+
+use pest::Parser as _;
+
+#[derive(pest_derive::Parser)]
+#[grammar = "shell.pest"]
+struct Shell;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Commands {
+ pipelines: Vec<Pipeline>,
+ input_string: String,
+}
+
+impl Commands {
+ pub fn parse(full_cmd: &str) -> Result<Self, super::Error> {
+ Ok(Self::build_ast(
+ Shell::parse(Rule::line, full_cmd)
+ .map_err(|e| super::Error::new(full_cmd, anyhow::anyhow!(e)))?
+ .next()
+ .unwrap()
+ .into_inner()
+ .next()
+ .unwrap(),
+ ))
+ }
+
+ pub fn eval(self, env: &Env) -> super::Commands {
+ super::Commands {
+ pipelines: self
+ .pipelines
+ .into_iter()
+ .map(|pipeline| pipeline.eval(env))
+ .collect(),
+ }
+ }
+
+ pub fn input_string(&self) -> &str {
+ &self.input_string
+ }
+
+ fn build_ast(commands: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(commands.as_rule(), Rule::commands));
+ let input_string = commands.as_str().to_string();
+ Self {
+ pipelines: commands
+ .into_inner()
+ .map(Pipeline::build_ast)
+ .collect(),
+ input_string,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Pipeline {
+ exes: Vec<Exe>,
+ input_string: String,
+}
+
+impl Pipeline {
+ pub fn parse(pipeline: &str) -> Result<Self, super::Error> {
+ Ok(Self::build_ast(
+ Shell::parse(Rule::pipeline, pipeline)
+ .map_err(|e| super::Error::new(pipeline, anyhow::anyhow!(e)))?
+ .next()
+ .unwrap(),
+ ))
+ }
+
+ pub fn eval(self, env: &Env) -> super::Pipeline {
+ super::Pipeline {
+ exes: self.exes.into_iter().map(|exe| exe.eval(env)).collect(),
+ input_string: self.input_string,
+ }
+ }
+
+ fn build_ast(pipeline: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pipeline.as_rule(), Rule::pipeline));
+ let input_string = pipeline.as_str().to_string();
+ Self {
+ exes: pipeline.into_inner().map(Exe::build_ast).collect(),
+ input_string,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct Exe {
+ exe: Word,
+ args: Vec<Word>,
+ redirects: Vec<Redirect>,
+}
+
+impl Exe {
+ fn eval(self, env: &Env) -> super::Exe {
+ super::Exe {
+ exe: std::path::PathBuf::from(self.exe.eval(env)),
+ args: self.args.into_iter().map(|arg| arg.eval(env)).collect(),
+ redirects: self
+ .redirects
+ .into_iter()
+ .map(|redirect| redirect.eval(env))
+ .collect(),
+ }
+ }
+
+ fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pair.as_rule(), Rule::exe));
+ let mut iter = pair.into_inner();
+ let exe = match WordOrRedirect::build_ast(iter.next().unwrap()) {
+ WordOrRedirect::Word(word) => word,
+ WordOrRedirect::Redirect(_) => todo!(),
+ };
+ let (args, redirects): (_, Vec<_>) = iter
+ .map(WordOrRedirect::build_ast)
+ .partition(|word| matches!(word, WordOrRedirect::Word(_)));
+ let args = args
+ .into_iter()
+ .map(|word| match word {
+ WordOrRedirect::Word(word) => word,
+ WordOrRedirect::Redirect(_) => unreachable!(),
+ })
+ .collect();
+ let redirects = redirects
+ .into_iter()
+ .map(|word| match word {
+ WordOrRedirect::Word(_) => unreachable!(),
+ WordOrRedirect::Redirect(redirect) => redirect,
+ })
+ .collect();
+ Self {
+ exe,
+ args,
+ redirects,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct Word {
+ word: String,
+ interpolate: bool,
+ quoted: bool,
+}
+
+impl Word {
+ fn parse(s: &str, rule: Rule) -> Self {
+ let interpolate =
+ matches!(rule, Rule::bareword | Rule::double_string);
+ let quoted =
+ matches!(rule, Rule::single_string | Rule::double_string);
+ let mut word_str = s.to_string();
+ if interpolate {
+ word_str = strip_escape(&word_str);
+ } else {
+ word_str = strip_basic_escape(&word_str);
+ }
+ Self {
+ word: word_str,
+ interpolate,
+ quoted,
+ }
+ }
+
+ fn eval(self, _env: &Env) -> String {
+ // TODO
+ self.word
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct Redirect {
+ from: std::os::unix::io::RawFd,
+ to: RedirectTarget,
+ dir: super::Direction,
+}
+
+impl Redirect {
+ fn parse(prefix: &str, to: Word) -> Self {
+ let (from, dir) = if let Some(from) = prefix.strip_suffix(">>") {
+ (from, super::Direction::Append)
+ } else if let Some(from) = prefix.strip_suffix('>') {
+ (from, super::Direction::Out)
+ } else if let Some(from) = prefix.strip_suffix('<') {
+ (from, super::Direction::In)
+ } else {
+ unreachable!()
+ };
+ let from = if from.is_empty() {
+ match dir {
+ super::Direction::In => 0,
+ super::Direction::Out | super::Direction::Append => 1,
+ }
+ } else {
+ from.parse().unwrap()
+ };
+ #[allow(clippy::option_if_let_else)]
+ let to = if let Some(fd) = to.word.strip_prefix('&') {
+ RedirectTarget::Fd(fd.parse().unwrap())
+ } else {
+ RedirectTarget::File(to)
+ };
+ Self { from, to, dir }
+ }
+
+ fn eval(self, env: &Env) -> super::Redirect {
+ super::Redirect {
+ from: self.from,
+ to: self.to.eval(env),
+ dir: self.dir,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum RedirectTarget {
+ Fd(std::os::unix::io::RawFd),
+ File(Word),
+}
+
+impl RedirectTarget {
+ fn eval(self, env: &Env) -> super::RedirectTarget {
+ match self {
+ Self::Fd(fd) => super::RedirectTarget::Fd(fd),
+ Self::File(path) => super::RedirectTarget::File(
+ std::path::PathBuf::from(path.eval(env)),
+ ),
+ }
+ }
+}
+
+enum WordOrRedirect {
+ Word(Word),
+ Redirect(Redirect),
+}
+
+impl WordOrRedirect {
+ fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pair.as_rule(), Rule::word));
+ let mut inner = pair.into_inner();
+ let mut word = inner.next().unwrap();
+ let mut prefix = None;
+ if matches!(word.as_rule(), Rule::redir_prefix) {
+ prefix = Some(word.as_str().trim().to_string());
+ word = inner.next().unwrap();
+ }
+ assert!(matches!(
+ word.as_rule(),
+ Rule::bareword | Rule::single_string | Rule::double_string
+ ));
+ let word = Word::parse(word.as_str(), word.as_rule());
+ if let Some(prefix) = prefix {
+ Self::Redirect(Redirect::parse(&prefix, word))
+ } else {
+ Self::Word(word)
+ }
+ }
+}
+
+fn strip_escape(s: &str) -> String {
+ let mut new = String::new();
+ let mut escape = false;
+ for c in s.chars() {
+ if escape {
+ new.push(c);
+ escape = false;
+ } else {
+ match c {
+ '\\' => escape = true,
+ _ => new.push(c),
+ }
+ }
+ }
+ new
+}
+
+fn strip_basic_escape(s: &str) -> String {
+ let mut new = String::new();
+ let mut escape = false;
+ for c in s.chars() {
+ if escape {
+ match c {
+ '\\' | '\'' => {}
+ _ => new.push('\\'),
+ }
+ new.push(c);
+ escape = false;
+ } else {
+ match c {
+ '\\' => escape = true,
+ _ => new.push(c),
+ }
+ }
+ }
+ new
+}
+
+#[cfg(test)]
+impl From<std::os::unix::io::RawFd> for RedirectTarget {
+ fn from(fd: std::os::unix::io::RawFd) -> Self {
+ Self::Fd(fd)
+ }
+}
+
+#[allow(clippy::fallible_impl_from)]
+#[cfg(test)]
+impl From<Word> for RedirectTarget {
+ fn from(word: Word) -> Self {
+ Self::File(word)
+ }
+}
+
+#[cfg(test)]
+macro_rules! c {
+ ($input_string:expr, $($pipelines:expr),*) => {
+ Commands {
+ pipelines: vec![$($pipelines),*],
+ input_string: $input_string.to_string(),
+ }
+ };
+ }
+
+#[cfg(test)]
+macro_rules! p {
+ ($input_string:expr, $($exes:expr),*) => {
+ Pipeline {
+ exes: vec![$($exes),*],
+ input_string: $input_string.to_string(),
+ }
+ };
+ }
+
+#[cfg(test)]
+macro_rules! e {
+ ($word:expr) => {
+ Exe {
+ exe: $word,
+ args: vec![],
+ redirects: vec![],
+ }
+ };
+ ($word:expr, $($args:expr),*) => {
+ Exe {
+ exe: $word,
+ args: vec![$($args),*],
+ redirects: vec![],
+ }
+ };
+ ($word:expr ; $($redirects:expr),*) => {
+ Exe {
+ exe: $word,
+ args: vec![],
+ redirects: vec![$($redirects),*],
+ }
+ };
+ ($word:expr, $($args:expr),* ; $($redirects:expr),*) => {
+ Exe {
+ exe: $word,
+ args: vec![$($args),*],
+ redirects: vec![$($redirects),*],
+ }
+ };
+ }
+
+#[cfg(test)]
+macro_rules! r {
+ ($from:literal, $to:expr, $dir:ident) => {
+ Redirect {
+ from: $from,
+ to: $to.into(),
+ dir: super::Direction::$dir,
+ }
+ };
+}
+
+#[cfg(test)]
+macro_rules! w {
+ ($word:expr) => {
+ Word {
+ word: $word.to_string(),
+ interpolate: true,
+ quoted: false,
+ }
+ };
+ ($word:expr, $interpolate:expr) => {
+ Word {
+ word: $word.to_string(),
+ interpolate: $interpolate,
+ quoted: false,
+ }
+ };
+ ($word:expr, $interpolate:expr, $quoted:expr) => {
+ Word {
+ word: $word.to_string(),
+ interpolate: $interpolate,
+ quoted: $quoted,
+ }
+ };
+}
+
+#[cfg(test)]
+macro_rules! parse_eq {
+ ($line:literal, $parsed:expr) => {
+ assert_eq!(&Commands::parse($line).unwrap(), &$parsed)
+ };
+}
+
+#[test]
+fn test_basic() {
+ parse_eq!("foo", c!("foo", p!("foo", e!(w!("foo")))));
+ parse_eq!(
+ "foo bar",
+ c!("foo bar", p!("foo bar", e!(w!("foo"), w!("bar"))))
+ );
+ parse_eq!(
+ "foo bar baz",
+ c!(
+ "foo bar baz",
+ p!("foo bar baz", e!(w!("foo"), w!("bar"), w!("baz")))
+ )
+ );
+ parse_eq!(
+ "foo | bar",
+ c!("foo | bar", p!("foo | bar", e!(w!("foo")), e!(w!("bar"))))
+ );
+ parse_eq!(
+ "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar",
+ c!(
+ "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar",
+ p!("command ls", e!(w!("command"), w!("ls"))),
+ p!(
+ "perl -E 'say foo' | tr a-z A-Z",
+ e!(w!("perl"), w!("-E"), w!("say foo", false, true)),
+ e!(w!("tr"), w!("a-z"), w!("A-Z"))
+ ),
+ p!("builtin echo bar", e!(w!("builtin"), w!("echo"), w!("bar")))
+ )
+ );
+}
+
+#[test]
+fn test_whitespace() {
+ parse_eq!(" foo ", c!("foo", p!("foo", e!(w!("foo")))));
+ parse_eq!(
+ " foo # this is a comment",
+ c!("foo", p!("foo", e!(w!("foo"))))
+ );
+ parse_eq!("foo#comment", c!("foo", p!("foo", e!(w!("foo")))));
+ parse_eq!(
+ "foo;bar|baz;quux#comment",
+ c!(
+ "foo;bar|baz;quux",
+ p!("foo", e!(w!("foo"))),
+ p!("bar|baz", e!(w!("bar")), e!(w!("baz"))),
+ p!("quux", e!(w!("quux")))
+ )
+ );
+ parse_eq!(
+ "foo | bar ",
+ c!(
+ "foo | bar",
+ p!("foo | bar", e!(w!("foo")), e!(w!("bar")))
+ )
+ );
+ parse_eq!(
+ " abc def ghi |jkl mno| pqr stu; vwxyz # comment",
+ c!(
+ "abc def ghi |jkl mno| pqr stu; vwxyz",
+ p!(
+ "abc def ghi |jkl mno| pqr stu",
+ e!(w!("abc"), w!("def"), w!("ghi")),
+ e!(w!("jkl"), w!("mno")),
+ e!(w!("pqr"), w!("stu"))
+ ),
+ p!("vwxyz", e!(w!("vwxyz")))
+ )
+ );
+ parse_eq!(
+ "foo 'bar # baz' \"quux # not a comment\" # comment",
+ c!(
+ "foo 'bar # baz' \"quux # not a comment\"",
+ p!(
+ "foo 'bar # baz' \"quux # not a comment\"",
+ e!(
+ w!("foo"),
+ w!("bar # baz", false, true),
+ w!("quux # not a comment", true, true)
+ )
+ )
+ )
+ );
+}
+
+#[test]
+fn test_redirect() {
+ parse_eq!(
+ "foo > bar",
+ c!(
+ "foo > bar",
+ p!("foo > bar", e!(w!("foo") ; r!(1, w!("bar"), Out)))
+ )
+ );
+ parse_eq!(
+ "foo <bar",
+ c!(
+ "foo <bar",
+ p!("foo <bar", e!(w!("foo") ; r!(0, w!("bar"), In)))
+ )
+ );
+ parse_eq!(
+ "foo > /dev/null 2>&1",
+ c!(
+ "foo > /dev/null 2>&1",
+ p!(
+ "foo > /dev/null 2>&1",
+ e!(w!("foo") ; r!(1, w!("/dev/null"), Out), r!(2, 1, Out))
+ )
+ )
+ );
+ parse_eq!(
+ "foo >>bar",
+ c!(
+ "foo >>bar",
+ p!("foo >>bar", e!(w!("foo") ; r!(1, w!("bar"), Append)))
+ )
+ );
+ parse_eq!(
+ "foo >> bar",
+ c!(
+ "foo >> bar",
+ p!("foo >> bar", e!(w!("foo") ; r!(1, w!("bar"), Append)))
+ )
+ );
+ parse_eq!(
+ "foo > 'bar baz'",
+ c!(
+ "foo > 'bar baz'",
+ p!(
+ "foo > 'bar baz'",
+ e!(w!("foo") ; r!(1, w!("bar baz", false, true), Out))
+ )
+ )
+ );
+}
+
+#[test]
+fn test_escape() {
+ parse_eq!(
+ "foo\\ bar",
+ c!("foo\\ bar", p!("foo\\ bar", e!(w!("foo bar"))))
+ );
+ parse_eq!(
+ "'foo\\ bar'",
+ c!(
+ "'foo\\ bar'",
+ p!("'foo\\ bar'", e!(w!("foo\\ bar", false, true)))
+ )
+ );
+ parse_eq!(
+ "\"foo\\ bar\"",
+ c!(
+ "\"foo\\ bar\"",
+ p!("\"foo\\ bar\"", e!(w!("foo bar", true, true)))
+ )
+ );
+ parse_eq!(
+ "\"foo\\\"bar\"",
+ c!(
+ "\"foo\\\"bar\"",
+ p!("\"foo\\\"bar\"", e!(w!("foo\"bar", true, true)))
+ )
+ );
+ parse_eq!(
+ "'foo\\'bar\\\\'",
+ c!(
+ "'foo\\'bar\\\\'",
+ p!("'foo\\'bar\\\\'", e!(w!("foo'bar\\", false, true)))
+ )
+ );
+ parse_eq!(
+ "foo > bar\\ baz",
+ c!(
+ "foo > bar\\ baz",
+ p!("foo > bar\\ baz", e!(w!("foo") ; r!(1, w!("bar baz"), Out)))
+ )
+ );
+}
diff --git a/src/pipeline/builtins/command.rs b/src/pipeline/builtins/command.rs
index c0fa86d..03fe8a2 100644
--- a/src/pipeline/builtins/command.rs
+++ b/src/pipeline/builtins/command.rs
@@ -8,12 +8,16 @@ pub struct Command {
impl Command {
pub fn new(exe: crate::parse::Exe) -> Result<Self, crate::parse::Exe> {
- if let Some(f) = super::BUILTINS.get(exe.exe()) {
- Ok(Self {
- exe,
- f,
- io: Io::new(),
- })
+ if let Some(s) = exe.exe().to_str() {
+ if let Some(f) = super::BUILTINS.get(s) {
+ Ok(Self {
+ exe,
+ f,
+ io: Io::new(),
+ })
+ } else {
+ Err(exe)
+ }
} else {
Err(exe)
}
diff --git a/src/pipeline/builtins/mod.rs b/src/pipeline/builtins/mod.rs
index 003892d..e608ae7 100644
--- a/src/pipeline/builtins/mod.rs
+++ b/src/pipeline/builtins/mod.rs
@@ -62,7 +62,7 @@ fn cd(
let dir = exe
.args()
- .into_iter()
+ .iter()
.map(std::convert::AsRef::as_ref)
.next()
.unwrap_or("");
@@ -137,8 +137,8 @@ fn echo(
}
};
}
- let count = exe.args().count();
- for (i, arg) in exe.args().enumerate() {
+ let count = exe.args().len();
+ for (i, arg) in exe.args().iter().enumerate() {
write_stdout!(arg.as_bytes());
if i == count - 1 {
write_stdout!(b"\n");
diff --git a/src/pipeline/command.rs b/src/pipeline/command.rs
index 746a340..f9dda06 100644
--- a/src/pipeline/command.rs
+++ b/src/pipeline/command.rs
@@ -2,27 +2,22 @@ use crate::pipeline::prelude::*;
pub struct Command {
inner: Inner,
- exe: String,
+ exe: std::path::PathBuf,
redirects: Vec<crate::parse::Redirect>,
pre_exec: Option<
Box<dyn FnMut() -> std::io::Result<()> + Send + Sync + 'static>,
>,
}
-pub enum Inner {
- Binary(async_std::process::Command),
- Builtin(super::builtins::Command),
-}
-
impl Command {
pub fn new(exe: crate::parse::Exe) -> Self {
- let exe_str = exe.exe().to_string();
+ let exe_path = exe.exe().to_path_buf();
let redirects = exe.redirects().to_vec();
Self {
inner: super::builtins::Command::new(exe).map_or_else(
|exe| Self::new_binary(exe).inner,
Inner::Builtin,
),
- exe: exe_str,
+ exe: exe_path,
redirects,
pre_exec: None,
}
@@ -30,25 +25,25 @@ impl Command {
#[allow(clippy::needless_pass_by_value)]
pub fn new_binary(exe: crate::parse::Exe) -> Self {
- let exe_str = exe.exe().to_string();
+ let exe_path = exe.exe().to_path_buf();
let redirects = exe.redirects().to_vec();
let mut cmd = async_std::process::Command::new(exe.exe());
cmd.args(exe.args());
Self {
inner: Inner::Binary(cmd),
- exe: exe_str,
+ exe: exe_path,
redirects,
pre_exec: None,
}
}
pub fn new_builtin(exe: crate::parse::Exe) -> Self {
- let exe_str = exe.exe().to_string();
+ let exe_path = exe.exe().to_path_buf();
let redirects = exe.redirects().to_vec();
Self {
inner: super::builtins::Command::new(exe)
.map_or_else(|_| todo!(), Inner::Builtin),
- exe: exe_str,
+ exe: exe_path,
redirects,
pre_exec: None,
}
@@ -132,7 +127,7 @@ impl Command {
anyhow::anyhow!(
"{}: {}",
crate::format::io_error(&e),
- exe
+ exe.display()
)
})?))
}
@@ -147,6 +142,11 @@ impl Command {
}
}
+pub enum Inner {
+ Binary(async_std::process::Command),
+ Builtin(super::builtins::Command),
+}
+
pub enum Child<'a> {
Binary(async_std::process::Child),
Builtin(super::builtins::Child<'a>),
diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs
index 015eeef..a54951c 100644
--- a/src/pipeline/mod.rs
+++ b/src/pipeline/mod.rs
@@ -37,7 +37,8 @@ async fn run_with_env(
env: &mut Env,
shell_write: &async_std::fs::File,
) -> anyhow::Result<()> {
- let pipeline = crate::parse::Pipeline::parse(env.pipeline().unwrap())?;
+ let pipeline =
+ crate::parse::ast::Pipeline::parse(env.pipeline().unwrap())?;
let (children, pg) = spawn_children(pipeline, env)?;
let status = wait_children(children, pg, env, shell_write).await;
env.set_status(status);
@@ -61,9 +62,10 @@ async fn write_event(
}
fn spawn_children(
- pipeline: crate::parse::Pipeline,
+ pipeline: crate::parse::ast::Pipeline,
env: &Env,
) -> anyhow::Result<(Vec<Child>, Option<nix::unistd::Pid>)> {
+ let pipeline = pipeline.eval(env);
let mut cmds: Vec<_> = pipeline.into_exes().map(Command::new).collect();
for i in 0..(cmds.len() - 1) {
let (r, w) = pipe()?;
diff --git a/src/shell/history/mod.rs b/src/shell/history/mod.rs
index 7cd37fc..8ff9ccc 100644
--- a/src/shell/history/mod.rs
+++ b/src/shell/history/mod.rs
@@ -88,7 +88,7 @@ impl History {
pub async fn run(
&mut self,
- ast: crate::parse::Commands,
+ ast: crate::parse::ast::Commands,
env: &Env,
event_w: async_std::channel::Sender<Event>,
) -> anyhow::Result<usize> {
@@ -269,7 +269,7 @@ impl std::iter::DoubleEndedIterator for VisibleEntries {
}
fn run_commands(
- ast: crate::parse::Commands,
+ ast: crate::parse::ast::Commands,
entry: async_std::sync::Arc<async_std::sync::Mutex<Entry>>,
mut env: Env,
input_r: async_std::channel::Receiver<Vec<u8>>,
@@ -299,7 +299,8 @@ fn run_commands(
}
};
- for pipeline in ast.pipelines() {
+ let commands = ast.eval(&env);
+ for pipeline in commands.pipelines() {
env.set_pipeline(pipeline.input_string().to_string());
match run_pipeline(&pty, &mut env, event_w.clone()).await {
Ok((pipeline_status, done)) => {
diff --git a/src/shell/mod.rs b/src/shell/mod.rs
index 2b8c849..2e28ddb 100644
--- a/src/shell/mod.rs
+++ b/src/shell/mod.rs
@@ -582,8 +582,8 @@ impl Shell {
fn parse(
&self,
cmd: &str,
- ) -> Result<crate::parse::Commands, crate::parse::Error> {
- let ast = crate::parse::Commands::parse(cmd)?;
+ ) -> Result<crate::parse::ast::Commands, crate::parse::Error> {
+ let ast = crate::parse::ast::Commands::parse(cmd)?;
// todo: interpolate
Ok(ast)
}
diff --git a/src/test_parse.rs b/src/test_parse.rs
deleted file mode 100644
index a43d359..0000000
--- a/src/test_parse.rs
+++ /dev/null
@@ -1,285 +0,0 @@
-#[allow(clippy::wildcard_imports)]
-use crate::parse::*;
-
-impl From<std::os::unix::io::RawFd> for RedirectTarget {
- fn from(fd: std::os::unix::io::RawFd) -> Self {
- Self::Fd(fd)
- }
-}
-
-impl From<std::path::PathBuf> for RedirectTarget {
- fn from(path: std::path::PathBuf) -> Self {
- Self::File(path)
- }
-}
-
-#[allow(clippy::fallible_impl_from)]
-impl From<&str> for RedirectTarget {
- fn from(path: &str) -> Self {
- Self::File(path.try_into().unwrap())
- }
-}
-
-macro_rules! c {
- ($input_string:expr, $($pipelines:expr),*) => {
- Commands {
- pipelines: vec![$($pipelines),*],
- input_string: $input_string.to_string(),
- }
- };
- }
-
-macro_rules! p {
- ($input_string:expr, $($exes:expr),*) => {
- Pipeline {
- exes: vec![$($exes),*],
- input_string: $input_string.to_string(),
- }
- };
- }
-
-macro_rules! e {
- ($word:expr) => {
- Exe {
- exe: $word,
- args: vec![],
- redirects: vec![],
- }
- };
- ($word:expr, $($args:expr),*) => {
- Exe {
- exe: $word,
- args: vec![$($args),*],
- redirects: vec![],
- }
- };
- ($word:expr ; $($redirects:expr),*) => {
- Exe {
- exe: $word,
- args: vec![],
- redirects: vec![$($redirects),*],
- }
- };
- ($word:expr, $($args:expr),* ; $($redirects:expr),*) => {
- Exe {
- exe: $word,
- args: vec![$($args),*],
- redirects: vec![$($redirects),*],
- }
- };
- }
-
-macro_rules! r {
- ($from:literal, $to:literal, $dir:ident) => {
- Redirect {
- from: $from,
- to: $to.into(),
- dir: Direction::$dir,
- }
- };
-}
-
-macro_rules! w {
- ($word:expr) => {
- Word {
- word: $word.to_string(),
- interpolate: true,
- quoted: false,
- }
- };
- ($word:expr, $interpolate:expr) => {
- Word {
- word: $word.to_string(),
- interpolate: $interpolate,
- quoted: false,
- }
- };
- ($word:expr, $interpolate:expr, $quoted:expr) => {
- Word {
- word: $word.to_string(),
- interpolate: $interpolate,
- quoted: $quoted,
- }
- };
-}
-
-macro_rules! parse_eq {
- ($line:literal, $parsed:expr) => {
- assert_eq!(&Commands::parse($line).unwrap(), &$parsed)
- };
-}
-
-#[test]
-fn test_basic() {
- parse_eq!("foo", c!("foo", p!("foo", e!(w!("foo")))));
- parse_eq!(
- "foo bar",
- c!("foo bar", p!("foo bar", e!(w!("foo"), w!("bar"))))
- );
- parse_eq!(
- "foo bar baz",
- c!(
- "foo bar baz",
- p!("foo bar baz", e!(w!("foo"), w!("bar"), w!("baz")))
- )
- );
- parse_eq!(
- "foo | bar",
- c!("foo | bar", p!("foo | bar", e!(w!("foo")), e!(w!("bar"))))
- );
- parse_eq!(
- "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar",
- c!(
- "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar",
- p!("command ls", e!(w!("command"), w!("ls"))),
- p!(
- "perl -E 'say foo' | tr a-z A-Z",
- e!(w!("perl"), w!("-E"), w!("say foo", false, true)),
- e!(w!("tr"), w!("a-z"), w!("A-Z"))
- ),
- p!("builtin echo bar", e!(w!("builtin"), w!("echo"), w!("bar")))
- )
- );
-}
-
-#[test]
-fn test_whitespace() {
- parse_eq!(" foo ", c!("foo", p!("foo", e!(w!("foo")))));
- parse_eq!(
- " foo # this is a comment",
- c!("foo", p!("foo", e!(w!("foo"))))
- );
- parse_eq!("foo#comment", c!("foo", p!("foo", e!(w!("foo")))));
- parse_eq!(
- "foo;bar|baz;quux#comment",
- c!(
- "foo;bar|baz;quux",
- p!("foo", e!(w!("foo"))),
- p!("bar|baz", e!(w!("bar")), e!(w!("baz"))),
- p!("quux", e!(w!("quux")))
- )
- );
- parse_eq!(
- "foo | bar ",
- c!(
- "foo | bar",
- p!("foo | bar", e!(w!("foo")), e!(w!("bar")))
- )
- );
- parse_eq!(
- " abc def ghi |jkl mno| pqr stu; vwxyz # comment",
- c!(
- "abc def ghi |jkl mno| pqr stu; vwxyz",
- p!(
- "abc def ghi |jkl mno| pqr stu",
- e!(w!("abc"), w!("def"), w!("ghi")),
- e!(w!("jkl"), w!("mno")),
- e!(w!("pqr"), w!("stu"))
- ),
- p!("vwxyz", e!(w!("vwxyz")))
- )
- );
- parse_eq!(
- "foo 'bar # baz' \"quux # not a comment\" # comment",
- c!(
- "foo 'bar # baz' \"quux # not a comment\"",
- p!(
- "foo 'bar # baz' \"quux # not a comment\"",
- e!(
- w!("foo"),
- w!("bar # baz", false, true),
- w!("quux # not a comment", true, true)
- )
- )
- )
- );
-}
-
-#[test]
-fn test_redirect() {
- parse_eq!(
- "foo > bar",
- c!(
- "foo > bar",
- p!("foo > bar", e!(w!("foo") ; r!(1, "bar", Out)))
- )
- );
- parse_eq!(
- "foo <bar",
- c!("foo <bar", p!("foo <bar", e!(w!("foo") ; r!(0, "bar", In))))
- );
- parse_eq!(
- "foo > /dev/null 2>&1",
- c!(
- "foo > /dev/null 2>&1",
- p!(
- "foo > /dev/null 2>&1",
- e!(w!("foo") ; r!(1, "/dev/null", Out), r!(2, 1, Out))
- )
- )
- );
- parse_eq!(
- "foo >>bar",
- c!(
- "foo >>bar",
- p!("foo >>bar", e!(w!("foo") ; r!(1, "bar", Append)))
- )
- );
- parse_eq!(
- "foo >> bar",
- c!(
- "foo >> bar",
- p!("foo >> bar", e!(w!("foo") ; r!(1, "bar", Append)))
- )
- );
- parse_eq!(
- "foo > 'bar baz'",
- c!(
- "foo > 'bar baz'",
- p!("foo > 'bar baz'", e!(w!("foo") ; r!(1, "bar baz", Out)))
- )
- );
-}
-
-#[test]
-fn test_escape() {
- parse_eq!(
- "foo\\ bar",
- c!("foo\\ bar", p!("foo\\ bar", e!(w!("foo bar"))))
- );
- parse_eq!(
- "'foo\\ bar'",
- c!(
- "'foo\\ bar'",
- p!("'foo\\ bar'", e!(w!("foo\\ bar", false, true)))
- )
- );
- parse_eq!(
- "\"foo\\ bar\"",
- c!(
- "\"foo\\ bar\"",
- p!("\"foo\\ bar\"", e!(w!("foo bar", true, true)))
- )
- );
- parse_eq!(
- "\"foo\\\"bar\"",
- c!(
- "\"foo\\\"bar\"",
- p!("\"foo\\\"bar\"", e!(w!("foo\"bar", true, true)))
- )
- );
- parse_eq!(
- "'foo\\'bar\\\\'",
- c!(
- "'foo\\'bar\\\\'",
- p!("'foo\\'bar\\\\'", e!(w!("foo'bar\\", false, true)))
- )
- );
- parse_eq!(
- "foo > bar\\ baz",
- c!(
- "foo > bar\\ baz",
- p!("foo > bar\\ baz", e!(w!("foo") ; r!(1, "bar baz", Out)))
- )
- );
-}