summaryrefslogtreecommitdiffstats
path: root/src/parse
diff options
context:
space:
mode:
Diffstat (limited to 'src/parse')
-rw-r--r--src/parse/ast.rs600
-rw-r--r--src/parse/mod.rs169
-rw-r--r--src/parse/test_ast.rs507
3 files changed, 1276 insertions, 0 deletions
diff --git a/src/parse/ast.rs b/src/parse/ast.rs
new file mode 100644
index 0000000..5bceed5
--- /dev/null
+++ b/src/parse/ast.rs
@@ -0,0 +1,600 @@
+use crate::prelude::*;
+
+use pest::Parser as _;
+
+#[derive(pest_derive::Parser)]
+#[grammar = "shell.pest"]
+struct Shell;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Commands {
+ commands: Vec<Command>,
+}
+
+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.to_string(), e))?
+ .next()
+ .unwrap()
+ .into_inner()
+ .next()
+ .unwrap(),
+ ))
+ }
+
+ pub fn commands(&self) -> &[Command] {
+ &self.commands
+ }
+
+ fn build_ast(commands: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(commands.as_rule(), Rule::commands));
+ Self {
+ commands: commands.into_inner().map(Command::build_ast).collect(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Command {
+ Pipeline(Pipeline),
+ If(Pipeline),
+ While(Pipeline),
+ For(String, Vec<Word>),
+ Else(Option<Pipeline>),
+ End,
+}
+
+impl Command {
+ fn build_ast(command: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(command.as_rule(), Rule::command));
+ let next = command.into_inner().next().unwrap();
+ match next.as_rule() {
+ Rule::pipeline => Self::Pipeline(Pipeline::build_ast(next)),
+ Rule::control => {
+ let ty = next.into_inner().next().unwrap();
+ match ty.as_rule() {
+ Rule::control_if => Self::If(Pipeline::build_ast(
+ ty.into_inner().next().unwrap(),
+ )),
+ Rule::control_while => Self::While(Pipeline::build_ast(
+ ty.into_inner().next().unwrap(),
+ )),
+ Rule::control_for => {
+ let mut inner = ty.into_inner();
+ let var = inner.next().unwrap();
+ assert!(matches!(var.as_rule(), Rule::bareword));
+ let list = inner.next().unwrap();
+ assert!(matches!(list.as_rule(), Rule::list));
+ let vals =
+ list.into_inner().map(Word::build_ast).collect();
+ Self::For(var.as_str().to_string(), vals)
+ }
+ Rule::control_else => Self::Else(
+ ty.into_inner().next().map(Pipeline::build_ast),
+ ),
+ Rule::control_end => Self::End,
+ _ => unreachable!(),
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Pipeline {
+ exes: Vec<Exe>,
+ span: (usize, usize),
+}
+
+impl Pipeline {
+ pub async fn eval(self, env: &Env) -> Result<super::Pipeline> {
+ Ok(super::Pipeline {
+ exes: self
+ .exes
+ .into_iter()
+ .map(|exe| exe.eval(env))
+ .collect::<futures_util::stream::FuturesOrdered<_>>()
+ .try_collect()
+ .await?,
+ })
+ }
+
+ pub fn span(&self) -> (usize, usize) {
+ self.span
+ }
+
+ fn build_ast(pipeline: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pipeline.as_rule(), Rule::pipeline));
+ let span = (pipeline.as_span().start(), pipeline.as_span().end());
+ Self {
+ exes: pipeline.into_inner().map(Exe::build_ast).collect(),
+ span,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Exe {
+ exe: Word,
+ args: Vec<Word>,
+ redirects: Vec<Redirect>,
+}
+
+impl Exe {
+ pub async fn eval(self, env: &Env) -> Result<super::Exe> {
+ let exe = self.exe.eval(env).await?;
+ assert_eq!(exe.len(), 1); // TODO
+ let exe = &exe[0];
+ Ok(super::Exe {
+ exe: std::path::PathBuf::from(exe),
+ args: self
+ .args
+ .into_iter()
+ .map(|arg| async {
+ arg.eval(env).await.map(IntoIterator::into_iter)
+ })
+ .collect::<futures_util::stream::FuturesOrdered<_>>()
+ .try_collect::<Vec<_>>()
+ .await?
+ .into_iter()
+ .flatten()
+ .collect(),
+ redirects: self
+ .redirects
+ .into_iter()
+ .map(|arg| arg.eval(env))
+ .collect::<futures_util::stream::FuturesOrdered<_>>()
+ .try_collect()
+ .await?,
+ })
+ }
+
+ pub fn parse(s: &str) -> Result<Self, super::Error> {
+ Ok(Self::build_ast(
+ Shell::parse(Rule::exe, s)
+ .map_err(|e| super::Error::new(s.to_string(), e))?
+ .next()
+ .unwrap(),
+ ))
+ }
+
+ fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pair.as_rule(), Rule::subshell | Rule::exe));
+ if matches!(pair.as_rule(), Rule::subshell) {
+ let mut iter = pair.into_inner();
+ let commands = iter.next().unwrap();
+ assert!(matches!(commands.as_rule(), Rule::commands));
+ let redirects = iter.map(Redirect::build_ast).collect();
+ return Self {
+ exe: Word {
+ parts: vec![WordPart::SingleQuoted(
+ crate::info::current_exe()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .to_string(),
+ )],
+ },
+ args: vec![
+ Word {
+ parts: vec![WordPart::SingleQuoted("-c".to_string())],
+ },
+ Word {
+ parts: vec![WordPart::SingleQuoted(
+ commands.as_str().to_string(),
+ )],
+ },
+ ],
+ redirects,
+ };
+ }
+ let mut iter = pair.into_inner();
+ let exe = iter.next().unwrap();
+ let exe = match exe.as_rule() {
+ Rule::word => Word::build_ast(exe),
+ Rule::redirect => todo!(),
+ _ => unreachable!(),
+ };
+ let mut args = vec![];
+ let mut redirects = vec![];
+ for arg in iter {
+ match arg.as_rule() {
+ Rule::word => args.push(Word::build_ast(arg)),
+ Rule::redirect => redirects.push(Redirect::build_ast(arg)),
+ _ => unreachable!(),
+ }
+ }
+ Self {
+ exe,
+ args,
+ redirects,
+ }
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for Exe {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct Visitor;
+ impl<'de> serde::de::Visitor<'de> for Visitor {
+ type Value = Exe;
+
+ fn expecting(
+ &self,
+ f: &mut std::fmt::Formatter,
+ ) -> std::fmt::Result {
+ f.write_str("a command")
+ }
+
+ fn visit_str<E>(
+ self,
+ value: &str,
+ ) -> std::result::Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ Exe::parse(value).map_err(serde::de::Error::custom)
+ }
+ }
+ deserializer.deserialize_string(Visitor)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Word {
+ parts: Vec<WordPart>,
+}
+
+impl Word {
+ pub async fn eval(self, env: &Env) -> Result<Vec<String>> {
+ let mut opts = glob::MatchOptions::new();
+ opts.require_literal_separator = true;
+ opts.require_literal_leading_dot = true;
+
+ let mut alternations: Vec<Vec<Vec<WordPart>>> = vec![];
+ let mut cur: Vec<WordPart> = vec![];
+ for part in self.parts {
+ if let WordPart::Alternation(words) = part {
+ if !cur.is_empty() {
+ alternations.push(vec![cur.clone()]);
+ cur.clear();
+ }
+ alternations
+ .push(words.into_iter().map(|word| word.parts).collect());
+ } else {
+ cur.push(part.clone());
+ }
+ }
+ if !cur.is_empty() {
+ alternations.push(vec![cur]);
+ }
+ let mut words: Vec<Vec<WordPart>> = std::iter::repeat(vec![])
+ .take(alternations.iter().map(Vec::len).product())
+ .collect();
+ for i in 0..words.len() {
+ let mut len = words.len();
+ for alternation in &alternations {
+ let idx = (i * alternation.len() / len) % alternation.len();
+ words[i].extend(alternation[idx].clone().into_iter());
+ len /= alternation.len();
+ }
+ }
+
+ let mut expanded_words = vec![];
+ for word in words {
+ let mut s = String::new();
+ let mut pat = String::new();
+ let mut is_glob = false;
+ let initial_bareword = word
+ .get(0)
+ .map_or(false, |part| matches!(part, WordPart::Bareword(_)));
+ for part in word {
+ match part {
+ WordPart::Alternation(_) => unreachable!(),
+ WordPart::Bareword(_) => {
+ let part = part.eval(env).await;
+ s.push_str(&part);
+ pat.push_str(&part);
+ if part.contains(&['*', '?', '['][..]) {
+ is_glob = true;
+ }
+ }
+ WordPart::Substitution(_)
+ | WordPart::Var(_)
+ | WordPart::DoubleQuoted(_)
+ | WordPart::SingleQuoted(_) => {
+ let part = part.eval(env).await;
+ s.push_str(&part);
+ pat.push_str(&glob::Pattern::escape(&part));
+ }
+ }
+ }
+ if initial_bareword {
+ s = expand_home(&s)?;
+ pat = expand_home(&pat)?;
+ }
+ if is_glob {
+ let mut found = false;
+ for file in glob::glob_with(&pat, opts)? {
+ let file = file?;
+ let s = file.to_str().unwrap();
+ if s == "."
+ || s == ".."
+ || s.ends_with("/.")
+ || s.ends_with("/..")
+ {
+ continue;
+ }
+ found = true;
+ expanded_words.push(s.to_string());
+ }
+ if !found {
+ anyhow::bail!("no matches for {}", s);
+ }
+ } else {
+ expanded_words.push(s);
+ }
+ }
+ Ok(expanded_words)
+ }
+
+ fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(
+ pair.as_rule(),
+ Rule::word | Rule::alternation_word
+ ));
+ Self {
+ parts: pair.into_inner().flat_map(WordPart::build_ast).collect(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum WordPart {
+ Alternation(Vec<Word>),
+ Substitution(String),
+ Var(String),
+ Bareword(String),
+ DoubleQuoted(String),
+ SingleQuoted(String),
+}
+
+impl WordPart {
+ async fn eval(self, env: &Env) -> String {
+ match self {
+ Self::Alternation(_) => unreachable!(),
+ Self::Substitution(commands) => {
+ let mut cmd = tokio::process::Command::new(
+ crate::info::current_exe().unwrap(),
+ );
+ cmd.args(&["-c", &commands]);
+ cmd.stdin(std::process::Stdio::inherit());
+ cmd.stderr(std::process::Stdio::inherit());
+ let mut out =
+ String::from_utf8(cmd.output().await.unwrap().stdout)
+ .unwrap();
+ if out.ends_with('\n') {
+ out.truncate(out.len() - 1);
+ }
+ out
+ }
+ Self::Var(name) => {
+ env.var(&name).unwrap_or_else(|| "".to_string())
+ }
+ Self::Bareword(s)
+ | Self::DoubleQuoted(s)
+ | Self::SingleQuoted(s) => s,
+ }
+ }
+
+ fn build_ast(
+ pair: pest::iterators::Pair<Rule>,
+ ) -> impl Iterator<Item = Self> + '_ {
+ assert!(matches!(
+ pair.as_rule(),
+ Rule::word_part | Rule::alternation_word_part
+ ));
+ pair.into_inner().map(|pair| match pair.as_rule() {
+ Rule::substitution => {
+ let commands = pair.into_inner().next().unwrap();
+ assert!(matches!(commands.as_rule(), Rule::commands));
+ Self::Substitution(commands.as_str().to_string())
+ }
+ Rule::var => {
+ let s = pair.as_str();
+ let inner = s.strip_prefix('$').unwrap();
+ Self::Var(
+ inner
+ .strip_prefix('{')
+ .map_or(inner, |inner| {
+ inner.strip_suffix('}').unwrap()
+ })
+ .to_string(),
+ )
+ }
+ Rule::bareword | Rule::alternation_bareword => {
+ Self::Bareword(strip_escape(pair.as_str()))
+ }
+ Rule::double_string => {
+ Self::DoubleQuoted(strip_escape(pair.as_str()))
+ }
+ Rule::single_string => {
+ Self::SingleQuoted(strip_basic_escape(pair.as_str()))
+ }
+ Rule::alternation => Self::Alternation(
+ pair.into_inner().map(Word::build_ast).collect(),
+ ),
+ _ => unreachable!(),
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct Redirect {
+ from: std::os::unix::io::RawFd,
+ to: Word,
+ dir: super::Direction,
+}
+
+impl Redirect {
+ fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self {
+ assert!(matches!(pair.as_rule(), Rule::redirect));
+ let mut iter = pair.into_inner();
+
+ let prefix = iter.next().unwrap().as_str();
+ let (from, dir) = prefix.strip_suffix(">>").map_or_else(
+ || {
+ prefix.strip_suffix('>').map_or_else(
+ || {
+ (
+ prefix.strip_suffix('<').unwrap(),
+ super::Direction::In,
+ )
+ },
+ |from| (from, super::Direction::Out),
+ )
+ },
+ |from| (from, super::Direction::Append),
+ );
+ let from = if from.is_empty() {
+ match dir {
+ super::Direction::In => 0,
+ super::Direction::Out | super::Direction::Append => 1,
+ }
+ } else {
+ parse_fd(from)
+ };
+
+ let to = Word::build_ast(iter.next().unwrap());
+
+ Self { from, to, dir }
+ }
+
+ async fn eval(self, env: &Env) -> Result<super::Redirect> {
+ let to = if self.to.parts.len() == 1 {
+ if let WordPart::Bareword(s) = &self.to.parts[0] {
+ if let Some(fd) = s.strip_prefix('&') {
+ super::RedirectTarget::Fd(parse_fd(fd))
+ } else {
+ let to = self.to.eval(env).await?;
+ assert_eq!(to.len(), 1); // TODO
+ let to = &to[0];
+ super::RedirectTarget::File(std::path::PathBuf::from(to))
+ }
+ } else {
+ let to = self.to.eval(env).await?;
+ assert_eq!(to.len(), 1); // TODO
+ let to = &to[0];
+ super::RedirectTarget::File(std::path::PathBuf::from(to))
+ }
+ } else {
+ let to = self.to.eval(env).await?;
+ assert_eq!(to.len(), 1); // TODO
+ let to = &to[0];
+ super::RedirectTarget::File(std::path::PathBuf::from(to))
+ };
+ Ok(super::Redirect {
+ from: self.from,
+ to,
+ dir: self.dir,
+ })
+ }
+}
+
+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
+}
+
+fn parse_fd(s: &str) -> std::os::unix::io::RawFd {
+ match s {
+ "in" => 0,
+ "out" => 1,
+ "err" => 2,
+ _ => s.parse().unwrap(),
+ }
+}
+
+fn expand_home(dir: &str) -> Result<String> {
+ if dir.starts_with('~') {
+ let path: std::path::PathBuf = dir.into();
+ if let std::path::Component::Normal(prefix) =
+ path.components().next().unwrap()
+ {
+ let prefix_bytes = prefix.as_bytes();
+ let name = if prefix_bytes == b"~" {
+ None
+ } else {
+ Some(std::ffi::OsStr::from_bytes(&prefix_bytes[1..]))
+ };
+ if let Some(home) = home(name) {
+ Ok(home
+ .join(path.strip_prefix(prefix).unwrap())
+ .to_str()
+ .unwrap()
+ .to_string())
+ } else {
+ anyhow::bail!(
+ "no such user: {}",
+ name.map(std::ffi::OsStr::to_string_lossy)
+ .as_ref()
+ .unwrap_or(&std::borrow::Cow::Borrowed("(deleted)"))
+ );
+ }
+ } else {
+ unreachable!()
+ }
+ } else {
+ Ok(dir.to_string())
+ }
+}
+
+fn home(user: Option<&std::ffi::OsStr>) -> Option<std::path::PathBuf> {
+ let user = user.map_or_else(
+ || users::get_user_by_uid(users::get_current_uid()),
+ users::get_user_by_name,
+ );
+ user.map(|user| user.home_dir().to_path_buf())
+}
+
+#[cfg(test)]
+#[path = "test_ast.rs"]
+mod test;
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
new file mode 100644
index 0000000..e2b7ec0
--- /dev/null
+++ b/src/parse/mod.rs
@@ -0,0 +1,169 @@
+pub mod ast;
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Pipeline {
+ exes: Vec<Exe>,
+}
+
+impl Pipeline {
+ pub fn into_exes(self) -> impl Iterator<Item = Exe> {
+ self.exes.into_iter()
+ }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+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 append(&mut self, other: Self) {
+ let Self {
+ exe: _exe,
+ args,
+ redirects,
+ } = other;
+ self.args.extend(args);
+ self.redirects.extend(redirects);
+ }
+
+ 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, Eq, PartialEq)]
+pub struct Redirect {
+ pub from: std::os::unix::io::RawFd,
+ pub to: RedirectTarget,
+ pub dir: Direction,
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum RedirectTarget {
+ Fd(std::os::unix::io::RawFd),
+ File(std::path::PathBuf),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ In,
+ Out,
+ Append,
+}
+
+impl Direction {
+ pub fn open(
+ self,
+ path: &std::path::Path,
+ ) -> nix::Result<std::os::unix::io::RawFd> {
+ use nix::fcntl::OFlag;
+ use nix::sys::stat::Mode;
+ Ok(match self {
+ Self::In => nix::fcntl::open(
+ path,
+ OFlag::O_NOCTTY | OFlag::O_RDONLY,
+ Mode::empty(),
+ )?,
+ Self::Out => nix::fcntl::open(
+ path,
+ OFlag::O_CREAT
+ | OFlag::O_NOCTTY
+ | OFlag::O_WRONLY
+ | OFlag::O_TRUNC,
+ Mode::S_IRUSR
+ | Mode::S_IWUSR
+ | Mode::S_IRGRP
+ | Mode::S_IWGRP
+ | Mode::S_IROTH
+ | Mode::S_IWOTH,
+ )?,
+ Self::Append => nix::fcntl::open(
+ path,
+ OFlag::O_APPEND
+ | OFlag::O_CREAT
+ | OFlag::O_NOCTTY
+ | OFlag::O_WRONLY,
+ Mode::S_IRUSR
+ | Mode::S_IWUSR
+ | Mode::S_IRGRP
+ | Mode::S_IWGRP
+ | Mode::S_IROTH
+ | Mode::S_IWOTH,
+ )?,
+ })
+ }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Error {
+ input: String,
+ e: pest::error::Error<ast::Rule>,
+}
+
+impl Error {
+ fn new(input: String, e: pest::error::Error<ast::Rule>) -> Self {
+ Self { input, e }
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self.e.variant {
+ pest::error::ErrorVariant::ParsingError {
+ positives,
+ negatives,
+ } => {
+ if !positives.is_empty() {
+ write!(f, "expected {:?}", positives[0])?;
+ for rule in &positives[1..] {
+ write!(f, ", {:?}", rule)?;
+ }
+ if !negatives.is_empty() {
+ write!(f, "; ")?;
+ }
+ }
+ if !negatives.is_empty() {
+ write!(f, "unexpected {:?}", negatives[0])?;
+ for rule in &negatives[1..] {
+ write!(f, ", {:?}", rule)?;
+ }
+ }
+ writeln!(f)?;
+ writeln!(f, "{}", self.input)?;
+ match &self.e.location {
+ pest::error::InputLocation::Pos(i) => {
+ write!(f, "{}^", " ".repeat(*i))?;
+ }
+ pest::error::InputLocation::Span((i, j)) => {
+ write!(f, "{}{}", " ".repeat(*i), "^".repeat(j - i))?;
+ }
+ }
+ }
+ pest::error::ErrorVariant::CustomError { message } => {
+ write!(f, "{}", message)?;
+ }
+ }
+ Ok(())
+ }
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ Some(&self.e)
+ }
+}
diff --git a/src/parse/test_ast.rs b/src/parse/test_ast.rs
new file mode 100644
index 0000000..a1f83dd
--- /dev/null
+++ b/src/parse/test_ast.rs
@@ -0,0 +1,507 @@
+use super::*;
+
+impl From<Pipeline> for Command {
+ fn from(pipeline: Pipeline) -> Self {
+ Self::Pipeline(pipeline)
+ }
+}
+
+macro_rules! cs {
+ ($($commands:expr),*) => {
+ Commands {
+ commands: [$($commands),*]
+ .into_iter()
+ .map(|c| c.into())
+ .collect(),
+ }
+ };
+}
+
+macro_rules! p {
+ ($span:expr, $($exes:expr),*) => {
+ Pipeline {
+ exes: vec![$($exes),*],
+ span: $span,
+ }
+ };
+}
+
+macro_rules! ep {
+ ($($exes:expr),*) => {
+ super::super::Pipeline {
+ exes: vec![$($exes),*],
+ }
+ };
+}
+
+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! ee {
+ ($exe:expr) => {
+ super::super::Exe {
+ exe: std::path::PathBuf::from($exe.to_string()),
+ args: vec![],
+ redirects: vec![],
+ }
+ };
+ ($exe:expr, $($args:expr),*) => {
+ super::super::Exe {
+ exe: std::path::PathBuf::from($exe.to_string()),
+ args: [$($args),*]
+ .into_iter()
+ .map(|s| s.to_string())
+ .collect(),
+ redirects: vec![],
+ }
+ };
+}
+
+macro_rules! r {
+ ($from:literal, $to:expr, $dir:ident) => {
+ Redirect {
+ from: $from,
+ to: $to,
+ dir: super::super::Direction::$dir,
+ }
+ };
+}
+
+macro_rules! w {
+ ($word:literal) => {
+ Word {
+ parts: vec![WordPart::Bareword($word.to_string())],
+ }
+ };
+ ($($word:expr),*) => {
+ Word {
+ parts: vec![$($word),*],
+ }
+ }
+}
+
+macro_rules! wpa {
+ ($($word:expr),*) => {
+ WordPart::Alternation(vec![$($word),*])
+ }
+}
+
+macro_rules! wpv {
+ ($var:literal) => {
+ WordPart::Var($var.to_string())
+ };
+}
+
+macro_rules! wpb {
+ ($bareword:expr) => {
+ WordPart::Bareword($bareword.to_string())
+ };
+}
+
+macro_rules! wpd {
+ ($doublequoted:expr) => {
+ WordPart::DoubleQuoted($doublequoted.to_string())
+ };
+}
+
+macro_rules! wps {
+ ($singlequoted:expr) => {
+ WordPart::SingleQuoted($singlequoted.to_string())
+ };
+}
+
+macro_rules! parse_eq {
+ ($line:literal, $parsed:expr) => {
+ assert_eq!(&Commands::parse($line).unwrap(), &$parsed)
+ };
+}
+
+macro_rules! eval_eq {
+ ($line:literal, $env:expr, $($evaled:expr),*) => {{
+ let ast = Commands::parse($line).unwrap();
+ let mut expected: Vec<super::super::Pipeline>
+ = vec![$($evaled),*];
+ for command in ast.commands {
+ let pipeline = match command {
+ Command::Pipeline(p)
+ | Command::If(p)
+ | Command::While(p) => p,
+ _ => continue,
+ };
+ assert_eq!(
+ pipeline.eval(&$env).await.unwrap(),
+ expected.remove(0)
+ );
+ }
+ }};
+}
+
+macro_rules! deserialize_eq {
+ ($line:literal, $parsed:expr) => {{
+ use serde::de::IntoDeserializer as _;
+ use serde::Deserialize as _;
+ let exe: Result<_, serde::de::value::Error> =
+ Exe::deserialize($line.into_deserializer());
+ assert_eq!(exe.unwrap(), $parsed);
+ }};
+}
+
+macro_rules! eval_fails {
+ ($line:literal, $env:expr) => {{
+ let ast = Commands::parse($line).unwrap();
+ let mut fail = false;
+ for command in ast.commands {
+ let pipeline = match command {
+ Command::Pipeline(p) | Command::If(p) | Command::While(p) => {
+ p
+ }
+ _ => continue,
+ };
+ if pipeline.eval(&$env).await.is_err() {
+ fail = true;
+ }
+ }
+ assert!(fail)
+ }};
+}
+
+#[test]
+fn test_basic() {
+ parse_eq!("foo", cs!(p!((0, 3), e!(w!("foo")))));
+ parse_eq!("foo bar", cs!(p!((0, 7), e!(w!("foo"), w!("bar")))));
+ parse_eq!(
+ "foo bar baz",
+ cs!(p!((0, 11), e!(w!("foo"), w!("bar"), w!("baz"))))
+ );
+ parse_eq!("foo | bar", cs!(p!((0, 9), e!(w!("foo")), e!(w!("bar")))));
+ parse_eq!(
+ "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar",
+ cs!(
+ p!((0, 10), e!(w!("command"), w!("ls"))),
+ p!(
+ (12, 42),
+ e!(w!("perl"), w!("-E"), w!(wps!("say foo"))),
+ e!(w!("tr"), w!("a-z"), w!("A-Z"))
+ ),
+ p!((44, 60), e!(w!("builtin"), w!("echo"), w!("bar")))
+ )
+ );
+
+ // XXX this parse may change in the future
+ let exe = crate::info::current_exe()
+ .unwrap()
+ .into_os_string()
+ .into_string()
+ .unwrap();
+ parse_eq!(
+ "seq 1 5 | (while read line; echo \"line: $line\"; end)",
+ cs!(p!(
+ (0, 52),
+ e!(w!("seq"), w!("1"), w!("5")),
+ e!(
+ w!(wps!(exe)),
+ w!(wps!("-c")),
+ w!(wps!("while read line; echo \"line: $line\"; end"))
+ )
+ ))
+ );
+
+ parse_eq!("foo ''", cs!(p!((0, 6), e!(w!("foo"), w!()))));
+ parse_eq!("foo \"\"", cs!(p!((0, 6), e!(w!("foo"), w!()))));
+}
+
+#[test]
+fn test_whitespace() {
+ parse_eq!(" foo ", cs!(p!((3, 6), e!(w!("foo")))));
+ parse_eq!(
+ " foo # this is a comment",
+ cs!(p!((3, 6), e!(w!("foo"))))
+ );
+ parse_eq!("foo#comment", cs!(p!((0, 3), e!(w!("foo")))));
+ parse_eq!(
+ "foo;bar|baz;quux#comment",
+ cs!(
+ p!((0, 3), e!(w!("foo"))),
+ p!((4, 11), e!(w!("bar")), e!(w!("baz"))),
+ p!((12, 16), e!(w!("quux")))
+ )
+ );
+ parse_eq!(
+ "foo | bar ",
+ cs!(p!((0, 12), e!(w!("foo")), e!(w!("bar"))))
+ );
+ parse_eq!(
+ " abc def ghi |jkl mno| pqr stu; vwxyz # comment",
+ cs!(
+ p!(
+ (2, 36),
+ e!(w!("abc"), w!("def"), w!("ghi")),
+ e!(w!("jkl"), w!("mno")),
+ e!(w!("pqr"), w!("stu"))
+ ),
+ p!((38, 43), e!(w!("vwxyz")))
+ )
+ );
+ parse_eq!(
+ "foo 'bar # baz' \"quux # not a comment\" # comment",
+ cs!(p!(
+ (0, 38),
+ e!(
+ w!("foo"),
+ w!(wps!("bar # baz")),
+ w!(wpd!("quux # not a comment"))
+ )
+ ))
+ );
+}
+
+#[test]
+fn test_redirect() {
+ parse_eq!(
+ "foo > bar",
+ cs!(p!((0, 9), e!(w!("foo") ; r!(1, w!("bar"), Out))))
+ );
+ parse_eq!(
+ "foo <bar",
+ cs!(p!((0, 8), e!(w!("foo") ; r!(0, w!("bar"), In))))
+ );
+ parse_eq!(
+ "foo > /dev/null 2>&1",
+ cs!(p!(
+ (0, 20),
+ e!(
+ w!("foo") ;
+ r!(1, w!("/dev/null"), Out), r!(2, w!("&1"), Out)
+ )
+ ))
+ );
+ parse_eq!(
+ "foo >>bar",
+ cs!(p!((0, 9), e!(w!("foo") ; r!(1, w!("bar"), Append))))
+ );
+ parse_eq!(
+ "foo >> bar",
+ cs!(p!((0, 10), e!(w!("foo") ; r!(1, w!("bar"), Append))))
+ );
+ parse_eq!(
+ "foo > 'bar baz'",
+ cs!(p!((0, 15), e!(w!("foo") ; r!(1, w!(wps!("bar baz")), Out))))
+ );
+}
+
+#[test]
+fn test_escape() {
+ parse_eq!("foo\\ bar", cs!(p!((0, 8), e!(w!("foo bar")))));
+ parse_eq!("'foo\\ bar'", cs!(p!((0, 10), e!(w!(wps!("foo\\ bar"))))));
+ parse_eq!("\"foo\\ bar\"", cs!(p!((0, 10), e!(w!(wpd!("foo bar"))))));
+ parse_eq!("\"foo\\\"bar\"", cs!(p!((0, 10), e!(w!(wpd!("foo\"bar"))))));
+ parse_eq!(
+ "'foo\\'bar\\\\'",
+ cs!(p!((0, 12), e!(w!(wps!("foo'bar\\")))))
+ );
+ parse_eq!(
+ "foo > bar\\ baz",
+ cs!(p!((0, 14), e!(w!("foo") ; r!(1, w!("bar baz"), Out))))
+ );
+}
+
+#[test]
+fn test_parts() {
+ parse_eq!(
+ "echo \"$HOME/bin\"",
+ cs!(p!((0, 16), e!(w!("echo"), w!(wpv!("HOME"), wpd!("/bin")))))
+ );
+ parse_eq!(
+ "echo \"dir: $HOME/bin\"",
+ cs!(p!(
+ (0, 21),
+ e!(w!("echo"), w!(wpd!("dir: "), wpv!("HOME"), wpd!("/bin")))
+ ))
+ );
+ parse_eq!(
+ "echo $HOME/bin",
+ cs!(p!((0, 14), e!(w!("echo"), w!(wpv!("HOME"), wpb!("/bin")))))
+ );
+ parse_eq!(
+ "echo '$HOME/bin'",
+ cs!(p!((0, 16), e!(w!("echo"), w!(wps!("$HOME/bin")))))
+ );
+ parse_eq!(
+ "echo \"foo\"\"bar\"",
+ cs!(p!((0, 15), e!(w!("echo"), w!(wpd!("foo"), wpd!("bar")))))
+ );
+ parse_eq!(
+ "echo $foo$bar$baz",
+ cs!(p!(
+ (0, 17),
+ e!(w!("echo"), w!(wpv!("foo"), wpv!("bar"), wpv!("baz")))
+ ))
+ );
+ parse_eq!(
+ "perl -E'say \"foo\"'",
+ cs!(p!(
+ (0, 18),
+ e!(w!("perl"), w!(wpb!("-E"), wps!("say \"foo\"")))
+ ))
+ );
+}
+
+#[test]
+fn test_alternation() {
+ parse_eq!(
+ "echo {foo,bar}",
+ cs!(p!((0, 14), e!(w!("echo"), w!(wpa!(w!("foo"), w!("bar"))))))
+ );
+ parse_eq!(
+ "echo {foo,bar}.rs",
+ cs!(p!(
+ (0, 17),
+ e!(w!("echo"), w!(wpa!(w!("foo"), w!("bar")), wpb!(".rs")))
+ ))
+ );
+ parse_eq!(
+ "echo {foo,bar,baz}.rs",
+ cs!(p!(
+ (0, 21),
+ e!(
+ w!("echo"),
+ w!(wpa!(w!("foo"), w!("bar"), w!("baz")), wpb!(".rs"))
+ )
+ ))
+ );
+ parse_eq!(
+ "echo {foo,}.rs",
+ cs!(p!(
+ (0, 14),
+ e!(w!("echo"), w!(wpa!(w!("foo"), w!()), wpb!(".rs")))
+ ))
+ );
+ parse_eq!(
+ "echo {foo}",
+ cs!(p!((0, 10), e!(w!("echo"), w!(wpa!(w!("foo"))))))
+ );
+ parse_eq!("echo {}", cs!(p!((0, 7), e!(w!("echo"), w!(wpa!(w!()))))));
+ parse_eq!(
+ "echo {foo,bar}.{rs,c}",
+ cs!(p!(
+ (0, 21),
+ e!(
+ w!("echo"),
+ w!(
+ wpa!(w!("foo"), w!("bar")),
+ wpb!("."),
+ wpa!(w!("rs"), w!("c"))
+ )
+ )
+ ))
+ );
+ parse_eq!(
+ "echo {$foo,\"${HOME}/bin\"}.{'r'\"s\",c}",
+ cs!(p!(
+ (0, 36),
+ e!(
+ w!("echo"),
+ w!(
+ wpa!(w!(wpv!("foo")), w!(wpv!("HOME"), wpd!("/bin"))),
+ wpb!("."),
+ wpa!(w!(wps!("r"), wpd!("s")), w!("c"))
+ )
+ )
+ ))
+ );
+}
+
+#[tokio::main]
+#[test]
+async fn test_eval_alternation() {
+ let mut env = Env::new().unwrap();
+ env.set_var("HOME", "/home/test");
+ env.set_var("foo", "value-of-foo");
+
+ eval_eq!("echo {foo,bar}", env, ep!(ee!("echo", "foo", "bar")));
+ eval_eq!(
+ "echo {foo,bar}.rs",
+ env,
+ ep!(ee!("echo", "foo.rs", "bar.rs"))
+ );
+ eval_eq!(
+ "echo {foo,bar,baz}.rs",
+ env,
+ ep!(ee!("echo", "foo.rs", "bar.rs", "baz.rs"))
+ );
+ eval_eq!("echo {foo,}.rs", env, ep!(ee!("echo", "foo.rs", ".rs")));
+ eval_eq!("echo {foo}", env, ep!(ee!("echo", "foo")));
+ eval_eq!("echo {}", env, ep!(ee!("echo", "")));
+ eval_eq!(
+ "echo {foo,bar}.{rs,c}",
+ env,
+ ep!(ee!("echo", "foo.rs", "foo.c", "bar.rs", "bar.c"))
+ );
+ eval_eq!(
+ "echo {$foo,\"${HOME}/bin\"}.{'r'\"s\",c}",
+ env,
+ ep!(ee!(
+ "echo",
+ "value-of-foo.rs",
+ "value-of-foo.c",
+ "/home/test/bin.rs",
+ "/home/test/bin.c"
+ ))
+ );
+}
+
+#[tokio::main]
+#[test]
+async fn test_eval_glob() {
+ let env = Env::new().unwrap();
+
+ eval_eq!(
+ "echo *.toml",
+ env,
+ ep!(ee!("echo", "Cargo.toml", "deny.toml"))
+ );
+ eval_eq!("echo .*.toml", env, ep!(ee!("echo", ".rustfmt.toml")));
+ eval_eq!(
+ "echo *.{lock,toml}",
+ env,
+ ep!(ee!("echo", "Cargo.lock", "Cargo.toml", "deny.toml"))
+ );
+ eval_eq!("echo foo]", env, ep!(ee!("echo", "foo]")));
+ eval_fails!("echo foo[", env);
+ eval_fails!("echo *.doesnotexist", env);
+ eval_fails!("echo *.{toml,doesnotexist}", env);
+}
+
+#[test]
+fn test_deserialize() {
+ deserialize_eq!("foo", e!(w!("foo")));
+ deserialize_eq!("foo bar baz", e!(w!("foo"), w!("bar"), w!("baz")));
+}