diff options
Diffstat (limited to 'src/parse')
-rw-r--r-- | src/parse/ast.rs | 600 | ||||
-rw-r--r-- | src/parse/mod.rs | 169 | ||||
-rw-r--r-- | src/parse/test_ast.rs | 507 |
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"))); +} |