use crate::parser::lex::Operator;
use crate::source::Location;
use itertools::Itertools;
use std::cell::RefCell;
use std::fmt;
use std::fmt::Write;
use std::os::unix::io::RawFd;
use std::rc::Rc;
type UnquoteResult = Result<bool, fmt::Error>;
pub trait Unquote {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult;
fn unquote(&self) -> (String, bool) {
let mut unquoted = String::new();
let is_quoted = self
.write_unquoted(&mut unquoted)
.expect("`write_unquoted` should not fail");
(unquoted, is_quoted)
}
}
pub trait MaybeLiteral {
fn extend_if_literal<T: Extend<char>>(&self, result: T) -> Result<T, T>;
fn to_string_if_literal(&self) -> Option<String> {
self.extend_if_literal(String::new()).ok()
}
}
impl<T: Unquote> Unquote for [T] {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
self.iter()
.try_fold(false, |quoted, item| Ok(quoted | item.write_unquoted(w)?))
}
}
impl<T: MaybeLiteral> MaybeLiteral for [T] {
fn extend_if_literal<R: Extend<char>>(&self, result: R) -> Result<R, R> {
self.iter()
.try_fold(result, |result, unit| unit.extend_if_literal(result))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SwitchType {
Alter,
Default,
Assign,
Error,
}
impl fmt::Display for SwitchType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SwitchType::*;
let c = match self {
Alter => '+',
Default => '-',
Assign => '=',
Error => '?',
};
f.write_char(c)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SwitchCondition {
Unset,
UnsetOrEmpty,
}
impl fmt::Display for SwitchCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SwitchCondition::*;
match self {
Unset => Ok(()),
UnsetOrEmpty => f.write_char(':'),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Switch {
pub r#type: SwitchType,
pub condition: SwitchCondition,
pub word: Word,
}
impl fmt::Display for Switch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.condition, self.r#type, self.word)
}
}
impl Unquote for Switch {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
write!(w, "{}{}", self.condition, self.r#type)?;
self.word.write_unquoted(w)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrimSide {
Prefix,
Suffix,
}
impl fmt::Display for TrimSide {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use TrimSide::*;
let c = match self {
Prefix => '#',
Suffix => '%',
};
f.write_char(c)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrimLength {
Shortest,
Longest,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Trim {
pub side: TrimSide,
pub length: TrimLength,
pub pattern: Word,
}
impl fmt::Display for Trim {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.side.fmt(f)?;
match self.length {
TrimLength::Shortest => (),
TrimLength::Longest => self.side.fmt(f)?,
}
self.pattern.fmt(f)
}
}
impl Unquote for Trim {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
write!(w, "{}", self.side)?;
match self.length {
TrimLength::Shortest => (),
TrimLength::Longest => write!(w, "{}", self.side)?,
}
self.pattern.write_unquoted(w)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Modifier {
None,
Length,
Switch(Switch),
Trim(Trim),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Param {
pub name: String,
pub modifier: Modifier,
pub location: Location,
}
impl fmt::Display for Param {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Modifier::*;
match self.modifier {
None => write!(f, "${{{}}}", self.name),
Length => write!(f, "${{#{}}}", self.name),
Switch(ref switch) => write!(f, "${{{}{}}}", self.name, switch),
Trim(ref trim) => write!(f, "${{{}{}}}", self.name, trim),
}
}
}
impl Unquote for Param {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
use Modifier::*;
match self.modifier {
None => {
write!(w, "${{{}}}", self.name)?;
Ok(false)
}
Length => {
write!(w, "${{#{}}}", self.name)?;
Ok(false)
}
Switch(ref switch) => {
write!(w, "${{{}", self.name)?;
let quoted = switch.write_unquoted(w)?;
w.write_char('}')?;
Ok(quoted)
}
Trim(ref trim) => {
write!(w, "${{{}", self.name)?;
let quoted = trim.write_unquoted(w)?;
w.write_char('}')?;
Ok(quoted)
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BackquoteUnit {
Literal(char),
Backslashed(char),
}
impl fmt::Display for BackquoteUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackquoteUnit::Literal(c) => write!(f, "{}", c),
BackquoteUnit::Backslashed(c) => write!(f, "\\{}", c),
}
}
}
impl Unquote for BackquoteUnit {
fn write_unquoted<W: std::fmt::Write>(&self, w: &mut W) -> UnquoteResult {
match self {
BackquoteUnit::Literal(c) => {
w.write_char(*c)?;
Ok(false)
}
BackquoteUnit::Backslashed(c) => {
w.write_char(*c)?;
Ok(true)
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TextUnit {
Literal(char),
Backslashed(char),
RawParam {
name: String,
location: Location,
},
BracedParam(Param),
CommandSubst {
content: Rc<str>,
location: Location,
},
Backquote {
content: Vec<BackquoteUnit>,
location: Location,
},
Arith {
content: Text,
location: Location,
},
}
pub use TextUnit::*;
impl fmt::Display for TextUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Literal(c) => write!(f, "{}", c),
Backslashed(c) => write!(f, "\\{}", c),
RawParam { name, .. } => write!(f, "${}", name),
BracedParam(param) => param.fmt(f),
CommandSubst { content, .. } => write!(f, "$({})", content),
Backquote { content, .. } => {
f.write_char('`')?;
content.iter().try_for_each(|unit| unit.fmt(f))?;
f.write_char('`')
}
Arith { content, .. } => write!(f, "$(({}))", content),
}
}
}
impl Unquote for TextUnit {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
match self {
Literal(c) => {
w.write_char(*c)?;
Ok(false)
}
Backslashed(c) => {
w.write_char(*c)?;
Ok(true)
}
RawParam { name, .. } => {
write!(w, "${}", name)?;
Ok(false)
}
BracedParam(param) => param.write_unquoted(w),
CommandSubst { content, .. } => {
write!(w, "$({})", content)?;
Ok(false)
}
Backquote { content, .. } => {
w.write_char('`')?;
let quoted = content.write_unquoted(w)?;
w.write_char('`')?;
Ok(quoted)
}
Arith { content, .. } => {
w.write_str("$((")?;
let quoted = content.write_unquoted(w)?;
w.write_str("))")?;
Ok(quoted)
}
}
}
}
impl MaybeLiteral for TextUnit {
fn extend_if_literal<T: Extend<char>>(&self, mut result: T) -> Result<T, T> {
if let Literal(c) = self {
result.extend(std::iter::once(*c));
Ok(result)
} else {
Err(result)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Text(pub Vec<TextUnit>);
impl Text {
pub fn from_literal_chars<I: IntoIterator<Item = char>>(i: I) -> Text {
Text(i.into_iter().map(Literal).collect())
}
}
impl fmt::Display for Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.iter().try_for_each(|unit| unit.fmt(f))
}
}
impl Unquote for Text {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
self.0.write_unquoted(w)
}
}
impl MaybeLiteral for Text {
fn extend_if_literal<T: Extend<char>>(&self, result: T) -> Result<T, T> {
self.0.extend_if_literal(result)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WordUnit {
Unquoted(TextUnit),
SingleQuote(String),
DoubleQuote(Text),
Tilde(String),
}
pub use WordUnit::*;
impl fmt::Display for WordUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unquoted(dq) => dq.fmt(f),
SingleQuote(s) => write!(f, "'{}'", s),
DoubleQuote(content) => write!(f, "\"{}\"", content),
Tilde(s) => write!(f, "~{}", s),
}
}
}
impl Unquote for WordUnit {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
match self {
Unquoted(inner) => inner.write_unquoted(w),
SingleQuote(inner) => {
w.write_str(inner)?;
Ok(true)
}
DoubleQuote(inner) => inner.write_unquoted(w),
Tilde(s) => {
write!(w, "~{}", s)?;
Ok(false)
}
}
}
}
impl MaybeLiteral for WordUnit {
fn extend_if_literal<T: Extend<char>>(&self, result: T) -> Result<T, T> {
if let Unquoted(inner) = self {
inner.extend_if_literal(result)
} else {
Err(result)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Word {
pub units: Vec<WordUnit>,
pub location: Location,
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.units.iter().try_for_each(|unit| write!(f, "{}", unit))
}
}
impl Unquote for Word {
fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
self.units.write_unquoted(w)
}
}
impl MaybeLiteral for Word {
fn extend_if_literal<T: Extend<char>>(&self, result: T) -> Result<T, T> {
self.units.extend_if_literal(result)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Value {
Scalar(Word),
Array(Vec<Word>),
}
pub use Value::*;
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Scalar(word) => word.fmt(f),
Array(words) => write!(f, "({})", words.iter().format(" ")),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Assign {
pub name: String,
pub value: Value,
pub location: Location,
}
impl fmt::Display for Assign {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}={}", &self.name, &self.value)
}
}
impl TryFrom<Word> for Assign {
type Error = Word;
fn try_from(mut word: Word) -> Result<Assign, Word> {
if let Some(eq) = word.units.iter().position(|u| u == &Unquoted(Literal('='))) {
if eq > 0 {
if let Some(name) = word.units[..eq].to_string_if_literal() {
assert!(!name.is_empty());
word.units.drain(..=eq);
word.parse_tilde_everywhere();
let location = word.location.clone();
let value = Scalar(word);
return Ok(Assign {
name,
value,
location,
});
}
}
}
Err(word)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Fd(pub RawFd);
impl Fd {
pub const STDIN: Fd = Fd(0);
pub const STDOUT: Fd = Fd(1);
pub const STDERR: Fd = Fd(2);
}
impl From<RawFd> for Fd {
fn from(raw_fd: RawFd) -> Fd {
Fd(raw_fd)
}
}
impl fmt::Display for Fd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RedirOp {
FileIn,
FileInOut,
FileOut,
FileAppend,
FileClobber,
FdIn,
FdOut,
Pipe,
String,
}
impl TryFrom<Operator> for RedirOp {
type Error = ();
fn try_from(op: Operator) -> Result<RedirOp, ()> {
use Operator::*;
use RedirOp::*;
match op {
Less => Ok(FileIn),
LessGreater => Ok(FileInOut),
Greater => Ok(FileOut),
GreaterGreater => Ok(FileAppend),
GreaterBar => Ok(FileClobber),
LessAnd => Ok(FdIn),
GreaterAnd => Ok(FdOut),
GreaterGreaterBar => Ok(Pipe),
LessLessLess => Ok(String),
_ => Err(()),
}
}
}
impl From<RedirOp> for Operator {
fn from(op: RedirOp) -> Operator {
use Operator::*;
use RedirOp::*;
match op {
FileIn => Less,
FileInOut => LessGreater,
FileOut => Greater,
FileAppend => GreaterGreater,
FileClobber => GreaterBar,
FdIn => LessAnd,
FdOut => GreaterAnd,
Pipe => GreaterGreaterBar,
String => LessLessLess,
}
}
}
impl fmt::Display for RedirOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Operator::from(*self).fmt(f)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HereDoc {
pub delimiter: Word,
pub remove_tabs: bool,
pub content: RefCell<Text>,
}
impl fmt::Display for HereDoc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(if self.remove_tabs { "<<-" } else { "<<" })?;
if let Some(Unquoted(Literal('-'))) = self.delimiter.units.get(0) {
f.write_char(' ')?;
}
write!(f, "{}", self.delimiter)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RedirBody {
Normal { operator: RedirOp, operand: Word },
HereDoc(Rc<HereDoc>),
}
impl RedirBody {
pub fn operand(&self) -> &Word {
match self {
RedirBody::Normal { operand, .. } => operand,
RedirBody::HereDoc(here_doc) => &here_doc.delimiter,
}
}
}
impl fmt::Display for RedirBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedirBody::Normal { operator, operand } => write!(f, "{}{}", operator, operand),
RedirBody::HereDoc(h) => write!(f, "{}", h),
}
}
}
impl<T: Into<Rc<HereDoc>>> From<T> for RedirBody {
fn from(t: T) -> Self {
RedirBody::HereDoc(t.into())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Redir {
pub fd: Option<Fd>,
pub body: RedirBody,
}
impl Redir {
pub fn fd_or_default(&self) -> Fd {
use RedirOp::*;
self.fd.unwrap_or(match self.body {
RedirBody::Normal { operator, .. } => match operator {
FileIn | FileInOut | FdIn | String => Fd::STDIN,
FileOut | FileAppend | FileClobber | FdOut | Pipe => Fd::STDOUT,
},
RedirBody::HereDoc { .. } => Fd::STDIN,
})
}
}
impl fmt::Display for Redir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(fd) = self.fd {
write!(f, "{}", fd)?;
}
write!(f, "{}", self.body)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimpleCommand {
pub assigns: Vec<Assign>,
pub words: Vec<Word>,
pub redirs: Rc<Vec<Redir>>,
}
impl SimpleCommand {
pub fn is_empty(&self) -> bool {
self.assigns.is_empty() && self.words.is_empty() && self.redirs.is_empty()
}
pub fn is_one_word(&self) -> bool {
self.assigns.is_empty() && self.words.len() == 1 && self.redirs.is_empty()
}
}
impl fmt::Display for SimpleCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let i1 = self.assigns.iter().map(|x| x as &dyn fmt::Display);
let i2 = self.words.iter().map(|x| x as &dyn fmt::Display);
let i3 = self.redirs.iter().map(|x| x as &dyn fmt::Display);
write!(f, "{}", i1.chain(i2).chain(i3).format(" "))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ElifThen {
pub condition: List,
pub body: List,
}
impl fmt::Display for ElifThen {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "elif {:#} then ", self.condition)?;
if f.alternate() {
write!(f, "{:#}", self.body)
} else {
write!(f, "{}", self.body)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CaseItem {
pub patterns: Vec<Word>,
pub body: List,
}
impl fmt::Display for CaseItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"({}) {};;",
self.patterns.iter().format(" | "),
self.body
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CompoundCommand {
Grouping(List),
Subshell { body: Rc<List>, location: Location },
For {
name: Word,
values: Option<Vec<Word>>,
body: List,
},
While { condition: List, body: List },
Until { condition: List, body: List },
If {
condition: List,
body: List,
elifs: Vec<ElifThen>,
r#else: Option<List>,
},
Case { subject: Word, items: Vec<CaseItem> },
}
impl fmt::Display for CompoundCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use CompoundCommand::*;
match self {
Grouping(list) => write!(f, "{{ {:#} }}", list),
Subshell { body, .. } => write!(f, "({})", body),
For { name, values, body } => {
write!(f, "for {}", name)?;
if let Some(values) = values {
f.write_str(" in")?;
for value in values {
write!(f, " {}", value)?;
}
f.write_char(';')?;
}
write!(f, " do {:#} done", body)
}
While { condition, body } => write!(f, "while {:#} do {:#} done", condition, body),
Until { condition, body } => write!(f, "until {:#} do {:#} done", condition, body),
If {
condition,
body,
elifs,
r#else,
} => {
write!(f, "if {:#} then {:#} ", condition, body)?;
for elif in elifs {
write!(f, "{:#} ", elif)?;
}
if let Some(r#else) = r#else {
write!(f, "else {:#} ", r#else)?;
}
f.write_str("fi")
}
Case { subject, items } => {
write!(f, "case {} in ", subject)?;
for item in items {
write!(f, "{} ", item)?;
}
f.write_str("esac")
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FullCompoundCommand {
pub command: CompoundCommand,
pub redirs: Vec<Redir>,
}
impl fmt::Display for FullCompoundCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let FullCompoundCommand { command, redirs } = self;
write!(f, "{}", command)?;
redirs.iter().try_for_each(|redir| write!(f, " {}", redir))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FunctionDefinition {
pub has_keyword: bool,
pub name: Word,
pub body: Rc<FullCompoundCommand>,
}
impl fmt::Display for FunctionDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.has_keyword {
f.write_str("function ")?;
}
write!(f, "{}() {}", self.name, self.body)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Command {
Simple(SimpleCommand),
Compound(FullCompoundCommand),
Function(FunctionDefinition),
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
match self {
Command::Simple(c) => c.fmt(f),
Command::Compound(c) => c.fmt(f),
Command::Function(c) => c.fmt(f),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Pipeline {
pub commands: Vec<Rc<Command>>,
pub negation: bool,
}
impl fmt::Display for Pipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
if self.negation {
write!(f, "! ")?;
}
write!(f, "{}", self.commands.iter().format(" | "))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AndOr {
AndThen,
OrElse,
}
impl TryFrom<Operator> for AndOr {
type Error = ();
fn try_from(op: Operator) -> Result<AndOr, ()> {
match op {
Operator::AndAnd => Ok(AndOr::AndThen),
Operator::BarBar => Ok(AndOr::OrElse),
_ => Err(()),
}
}
}
impl From<AndOr> for Operator {
fn from(op: AndOr) -> Operator {
match op {
AndOr::AndThen => Operator::AndAnd,
AndOr::OrElse => Operator::BarBar,
}
}
}
impl fmt::Display for AndOr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AndOr::AndThen => write!(f, "&&"),
AndOr::OrElse => write!(f, "||"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AndOrList {
pub first: Pipeline,
pub rest: Vec<(AndOr, Pipeline)>,
}
impl fmt::Display for AndOrList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.first)?;
self.rest
.iter()
.try_for_each(|(c, p)| write!(f, " {} {}", c, p))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Item {
pub and_or: Rc<AndOrList>,
pub async_flag: Option<Location>,
}
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.and_or)?;
if self.async_flag.is_some() {
write!(f, "&")
} else if f.alternate() {
write!(f, ";")
} else {
Ok(())
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct List(pub Vec<Item>);
impl fmt::Display for List {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some((last, others)) = self.0.split_last() {
for item in others {
write!(f, "{:#} ", item)?;
}
if f.alternate() {
write!(f, "{:#}", last)
} else {
write!(f, "{}", last)
}
} else {
Ok(())
}
}
}
#[allow(clippy::bool_assert_comparison)]
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use std::str::FromStr;
#[test]
fn switch_display() {
let switch = Switch {
r#type: SwitchType::Alter,
condition: SwitchCondition::Unset,
word: "".parse().unwrap(),
};
assert_eq!(switch.to_string(), "+");
let switch = Switch {
r#type: SwitchType::Default,
condition: SwitchCondition::UnsetOrEmpty,
word: "foo".parse().unwrap(),
};
assert_eq!(switch.to_string(), ":-foo");
let switch = Switch {
r#type: SwitchType::Assign,
condition: SwitchCondition::UnsetOrEmpty,
word: "bar baz".parse().unwrap(),
};
assert_eq!(switch.to_string(), ":=bar baz");
let switch = Switch {
r#type: SwitchType::Error,
condition: SwitchCondition::Unset,
word: "?error".parse().unwrap(),
};
assert_eq!(switch.to_string(), "??error");
}
#[test]
fn switch_unquote() {
let switch = Switch {
r#type: SwitchType::Default,
condition: SwitchCondition::UnsetOrEmpty,
word: "foo bar".parse().unwrap(),
};
let (unquoted, is_quoted) = switch.unquote();
assert_eq!(unquoted, ":-foo bar");
assert_eq!(is_quoted, false);
let switch = Switch {
r#type: SwitchType::Error,
condition: SwitchCondition::Unset,
word: r"e\r\ror".parse().unwrap(),
};
let (unquoted, is_quoted) = switch.unquote();
assert_eq!(unquoted, "?error");
assert_eq!(is_quoted, true);
}
#[test]
fn trim_display() {
let trim = Trim {
side: TrimSide::Prefix,
length: TrimLength::Shortest,
pattern: "foo".parse().unwrap(),
};
assert_eq!(trim.to_string(), "#foo");
let trim = Trim {
side: TrimSide::Prefix,
length: TrimLength::Longest,
pattern: "".parse().unwrap(),
};
assert_eq!(trim.to_string(), "##");
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Shortest,
pattern: "bar".parse().unwrap(),
};
assert_eq!(trim.to_string(), "%bar");
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Longest,
pattern: "*".parse().unwrap(),
};
assert_eq!(trim.to_string(), "%%*");
}
#[test]
fn trim_unquote() {
let trim = Trim {
side: TrimSide::Prefix,
length: TrimLength::Shortest,
pattern: "".parse().unwrap(),
};
let (unquoted, is_quoted) = trim.unquote();
assert_eq!(unquoted, "#");
assert_eq!(is_quoted, false);
let trim = Trim {
side: TrimSide::Prefix,
length: TrimLength::Longest,
pattern: "'yes'".parse().unwrap(),
};
let (unquoted, is_quoted) = trim.unquote();
assert_eq!(unquoted, "##yes");
assert_eq!(is_quoted, true);
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Shortest,
pattern: r"\no".parse().unwrap(),
};
let (unquoted, is_quoted) = trim.unquote();
assert_eq!(unquoted, "%no");
assert_eq!(is_quoted, true);
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Longest,
pattern: "?".parse().unwrap(),
};
let (unquoted, is_quoted) = trim.unquote();
assert_eq!(unquoted, "%%?");
assert_eq!(is_quoted, false);
}
#[test]
fn braced_param_display() {
let param = Param {
name: "foo".to_string(),
modifier: Modifier::None,
location: Location::dummy(""),
};
assert_eq!(param.to_string(), "${foo}");
let param = Param {
modifier: Modifier::Length,
..param
};
assert_eq!(param.to_string(), "${#foo}");
let switch = Switch {
r#type: SwitchType::Assign,
condition: SwitchCondition::UnsetOrEmpty,
word: "bar baz".parse().unwrap(),
};
let param = Param {
modifier: Modifier::Switch(switch),
..param
};
assert_eq!(param.to_string(), "${foo:=bar baz}");
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Shortest,
pattern: "baz' 'bar".parse().unwrap(),
};
let param = Param {
modifier: Modifier::Trim(trim),
..param
};
assert_eq!(param.to_string(), "${foo%baz' 'bar}");
}
#[test]
fn braced_param_unquote() {
let param = Param {
name: "foo".to_string(),
modifier: Modifier::None,
location: Location::dummy(""),
};
let (unquoted, is_quoted) = param.unquote();
assert_eq!(unquoted, "${foo}");
assert_eq!(is_quoted, false);
let param = Param {
modifier: Modifier::Length,
..param
};
let (unquoted, is_quoted) = param.unquote();
assert_eq!(unquoted, "${#foo}");
assert_eq!(is_quoted, false);
let switch = Switch {
r#type: SwitchType::Assign,
condition: SwitchCondition::UnsetOrEmpty,
word: "'bar'".parse().unwrap(),
};
let param = Param {
modifier: Modifier::Switch(switch),
..param
};
let (unquoted, is_quoted) = param.unquote();
assert_eq!(unquoted, "${foo:=bar}");
assert_eq!(is_quoted, true);
let trim = Trim {
side: TrimSide::Suffix,
length: TrimLength::Shortest,
pattern: "baz' 'bar".parse().unwrap(),
};
let param = Param {
modifier: Modifier::Trim(trim),
..param
};
let (unquoted, is_quoted) = param.unquote();
assert_eq!(unquoted, "${foo%baz bar}");
assert_eq!(is_quoted, true);
}
#[test]
fn backquote_unit_display() {
let literal = BackquoteUnit::Literal('A');
assert_eq!(literal.to_string(), "A");
let backslashed = BackquoteUnit::Backslashed('X');
assert_eq!(backslashed.to_string(), r"\X");
}
#[test]
fn backquote_unit_unquote() {
let literal = BackquoteUnit::Literal('A');
let (unquoted, is_quoted) = literal.unquote();
assert_eq!(unquoted, "A");
assert_eq!(is_quoted, false);
let backslashed = BackquoteUnit::Backslashed('X');
let (unquoted, is_quoted) = backslashed.unquote();
assert_eq!(unquoted, "X");
assert_eq!(is_quoted, true);
}
#[test]
fn text_unit_display() {
let literal = Literal('A');
assert_eq!(literal.to_string(), "A");
let backslashed = Backslashed('X');
assert_eq!(backslashed.to_string(), r"\X");
let raw_param = RawParam {
name: "PARAM".to_string(),
location: Location::dummy(""),
};
assert_eq!(raw_param.to_string(), "$PARAM");
let command_subst = CommandSubst {
content: r"foo\bar".into(),
location: Location::dummy(""),
};
assert_eq!(command_subst.to_string(), r"$(foo\bar)");
let backquote = Backquote {
content: vec![
BackquoteUnit::Literal('a'),
BackquoteUnit::Backslashed('b'),
BackquoteUnit::Backslashed('c'),
BackquoteUnit::Literal('d'),
],
location: Location::dummy(""),
};
assert_eq!(backquote.to_string(), r"`a\b\cd`");
let arith = Arith {
content: Text(vec![literal, backslashed, command_subst, backquote]),
location: Location::dummy(""),
};
assert_eq!(arith.to_string(), r"$((A\X$(foo\bar)`a\b\cd`))");
}
#[test]
fn text_from_literal_chars() {
let text = Text::from_literal_chars(['a', '1'].iter().copied());
assert_eq!(text.0, [Literal('a'), Literal('1')]);
}
#[test]
fn text_unquote_without_quotes() {
let empty = Text(vec![]);
let (unquoted, is_quoted) = empty.unquote();
assert_eq!(unquoted, "");
assert_eq!(is_quoted, false);
let nonempty = Text(vec![
Literal('W'),
RawParam {
name: "X".to_string(),
location: Location::dummy(""),
},
CommandSubst {
content: "Y".into(),
location: Location::dummy(""),
},
Backquote {
content: vec![BackquoteUnit::Literal('Z')],
location: Location::dummy(""),
},
Arith {
content: Text(vec![Literal('0')]),
location: Location::dummy(""),
},
]);
let (unquoted, is_quoted) = nonempty.unquote();
assert_eq!(unquoted, "W$X$(Y)`Z`$((0))");
assert_eq!(is_quoted, false);
}
#[test]
fn text_unquote_with_quotes() {
let quoted = Text(vec![
Literal('a'),
Backslashed('b'),
Literal('c'),
Arith {
content: Text(vec![Literal('d')]),
location: Location::dummy(""),
},
Literal('e'),
]);
let (unquoted, is_quoted) = quoted.unquote();
assert_eq!(unquoted, "abc$((d))e");
assert_eq!(is_quoted, true);
let content = vec![BackquoteUnit::Backslashed('X')];
let location = Location::dummy("");
let quoted = Text(vec![Backquote { content, location }]);
let (unquoted, is_quoted) = quoted.unquote();
assert_eq!(unquoted, "`X`");
assert_eq!(is_quoted, true);
let content = Text(vec![Backslashed('X')]);
let location = Location::dummy("");
let quoted = Text(vec![Arith { content, location }]);
let (unquoted, is_quoted) = quoted.unquote();
assert_eq!(unquoted, "$((X))");
assert_eq!(is_quoted, true);
}
#[test]
fn text_to_string_if_literal_success() {
let empty = Text(vec![]);
let s = empty.to_string_if_literal().unwrap();
assert_eq!(s, "");
let nonempty = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
let s = nonempty.to_string_if_literal().unwrap();
assert_eq!(s, "foo");
}
#[test]
fn text_to_string_if_literal_failure() {
let backslashed = Text(vec![Backslashed('a')]);
assert_eq!(backslashed.to_string_if_literal(), None);
}
#[test]
fn word_unit_display() {
let unquoted = Unquoted(Literal('A'));
assert_eq!(unquoted.to_string(), "A");
let unquoted = Unquoted(Backslashed('B'));
assert_eq!(unquoted.to_string(), "\\B");
let single_quote = SingleQuote("".to_string());
assert_eq!(single_quote.to_string(), "''");
let single_quote = SingleQuote(r#"a"b"c\"#.to_string());
assert_eq!(single_quote.to_string(), r#"'a"b"c\'"#);
let double_quote = DoubleQuote(Text(vec![]));
assert_eq!(double_quote.to_string(), "\"\"");
let double_quote = DoubleQuote(Text(vec![Literal('A'), Backslashed('B')]));
assert_eq!(double_quote.to_string(), "\"A\\B\"");
let tilde = Tilde("".to_string());
assert_eq!(tilde.to_string(), "~");
let tilde = Tilde("foo".to_string());
assert_eq!(tilde.to_string(), "~foo");
}
#[test]
fn word_unquote() {
let mut word = Word::from_str(r#"~a/b\c'd'"e""#).unwrap();
let (unquoted, is_quoted) = word.unquote();
assert_eq!(unquoted, "~a/bcde");
assert_eq!(is_quoted, true);
word.parse_tilde_front();
let (unquoted, is_quoted) = word.unquote();
assert_eq!(unquoted, "~a/bcde");
assert_eq!(is_quoted, true);
}
#[test]
fn word_to_string_if_literal_success() {
let empty = Word::from_str("").unwrap();
let s = empty.to_string_if_literal().unwrap();
assert_eq!(s, "");
let nonempty = Word::from_str("~foo").unwrap();
let s = nonempty.to_string_if_literal().unwrap();
assert_eq!(s, "~foo");
}
#[test]
fn word_to_string_if_literal_failure() {
let location = Location::dummy("foo");
let backslashed = Unquoted(Backslashed('?'));
let word = Word {
units: vec![backslashed],
location,
};
assert_eq!(word.to_string_if_literal(), None);
let word = Word {
units: vec![Tilde("foo".to_string())],
..word
};
assert_eq!(word.to_string_if_literal(), None);
}
#[test]
fn scalar_display() {
let s = Scalar(Word::from_str("my scalar value").unwrap());
assert_eq!(s.to_string(), "my scalar value");
}
#[test]
fn array_display_empty() {
let a = Array(vec![]);
assert_eq!(a.to_string(), "()");
}
#[test]
fn array_display_one() {
let a = Array(vec![Word::from_str("one").unwrap()]);
assert_eq!(a.to_string(), "(one)");
}
#[test]
fn array_display_many() {
let a = Array(vec![
Word::from_str("let").unwrap(),
Word::from_str("me").unwrap(),
Word::from_str("see").unwrap(),
]);
assert_eq!(a.to_string(), "(let me see)");
}
#[test]
fn assign_display() {
let mut a = Assign::from_str("foo=bar").unwrap();
assert_eq!(a.to_string(), "foo=bar");
a.value = Array(vec![]);
assert_eq!(a.to_string(), "foo=()");
}
#[test]
fn assign_try_from_word_without_equal() {
let word = Word::from_str("foo").unwrap();
let result = Assign::try_from(word.clone());
assert_eq!(result.unwrap_err(), word);
}
#[test]
fn assign_try_from_word_with_empty_name() {
let word = Word::from_str("=foo").unwrap();
let result = Assign::try_from(word.clone());
assert_eq!(result.unwrap_err(), word);
}
#[test]
fn assign_try_from_word_with_non_literal_name() {
let mut word = Word::from_str("night=foo").unwrap();
word.units.insert(0, Unquoted(Backslashed('k')));
let result = Assign::try_from(word.clone());
assert_eq!(result.unwrap_err(), word);
}
#[test]
fn assign_try_from_word_with_literal_name() {
let word = Word::from_str("night=foo").unwrap();
let location = word.location.clone();
let assign = Assign::try_from(word).unwrap();
assert_eq!(assign.name, "night");
assert_matches!(assign.value, Scalar(value) => {
assert_eq!(value.to_string(), "foo");
assert_eq!(value.location, location);
});
assert_eq!(assign.location, location);
}
#[test]
fn assign_try_from_word_tilde() {
let word = Word::from_str("a=~:~b").unwrap();
let assign = Assign::try_from(word).unwrap();
assert_matches!(assign.value, Scalar(value) => {
assert_eq!(
value.units,
[
WordUnit::Tilde("".to_string()),
WordUnit::Unquoted(TextUnit::Literal(':')),
WordUnit::Tilde("b".to_string()),
]
);
});
}
#[test]
fn redir_op_conversions() {
use RedirOp::*;
for op in &[
FileIn,
FileInOut,
FileOut,
FileAppend,
FileClobber,
FdIn,
FdOut,
Pipe,
String,
] {
let op2 = RedirOp::try_from(Operator::from(*op));
assert_eq!(op2, Ok(*op));
}
}
#[test]
fn here_doc_display() {
let heredoc = HereDoc {
delimiter: Word::from_str("END").unwrap(),
remove_tabs: true,
content: Text::from_str("here").unwrap().into(),
};
assert_eq!(heredoc.to_string(), "<<-END");
let heredoc = HereDoc {
delimiter: Word::from_str("XXX").unwrap(),
remove_tabs: false,
content: Text::from_str("there").unwrap().into(),
};
assert_eq!(heredoc.to_string(), "<<XXX");
}
#[test]
fn here_doc_display_disambiguation() {
let heredoc = HereDoc {
delimiter: Word::from_str("--").unwrap(),
remove_tabs: false,
content: Text::from_str("here").unwrap().into(),
};
assert_eq!(heredoc.to_string(), "<< --");
let heredoc = HereDoc {
delimiter: Word::from_str("-").unwrap(),
remove_tabs: true,
content: Text::from_str("here").unwrap().into(),
};
assert_eq!(heredoc.to_string(), "<<- -");
}
#[test]
fn redir_display() {
let heredoc = HereDoc {
delimiter: Word::from_str("END").unwrap(),
remove_tabs: false,
content: Text::from_str("here").unwrap().into(),
};
let redir = Redir {
fd: None,
body: heredoc.into(),
};
assert_eq!(redir.to_string(), "<<END");
let redir = Redir {
fd: Some(Fd(0)),
..redir
};
assert_eq!(redir.to_string(), "0<<END");
let redir = Redir {
fd: Some(Fd(9)),
..redir
};
assert_eq!(redir.to_string(), "9<<END");
}
#[test]
fn simple_command_display() {
let mut command = SimpleCommand {
assigns: vec![],
words: vec![],
redirs: vec![].into(),
};
assert_eq!(command.to_string(), "");
command
.assigns
.push(Assign::from_str("name=value").unwrap());
assert_eq!(command.to_string(), "name=value");
command
.assigns
.push(Assign::from_str("hello=world").unwrap());
assert_eq!(command.to_string(), "name=value hello=world");
command.words.push(Word::from_str("echo").unwrap());
assert_eq!(command.to_string(), "name=value hello=world echo");
command.words.push(Word::from_str("foo").unwrap());
assert_eq!(command.to_string(), "name=value hello=world echo foo");
Rc::make_mut(&mut command.redirs).push(Redir {
fd: None,
body: RedirBody::from(HereDoc {
delimiter: Word::from_str("END").unwrap(),
remove_tabs: false,
content: Text::from_str("").unwrap().into(),
}),
});
assert_eq!(command.to_string(), "name=value hello=world echo foo <<END");
command.assigns.clear();
assert_eq!(command.to_string(), "echo foo <<END");
command.words.clear();
assert_eq!(command.to_string(), "<<END");
Rc::make_mut(&mut command.redirs).push(Redir {
fd: Some(Fd(1)),
body: RedirBody::from(HereDoc {
delimiter: Word::from_str("here").unwrap(),
remove_tabs: true,
content: Text::from_str("ignored").unwrap().into(),
}),
});
assert_eq!(command.to_string(), "<<END 1<<-here");
command.assigns.push(Assign::from_str("foo=bar").unwrap());
assert_eq!(command.to_string(), "foo=bar <<END 1<<-here");
}
#[test]
fn elif_then_display() {
let condition: List = "c 1& c 2".parse().unwrap();
let body = "b 1& b 2".parse().unwrap();
let elif = ElifThen { condition, body };
assert_eq!(format!("{}", elif), "elif c 1& c 2; then b 1& b 2");
assert_eq!(format!("{:#}", elif), "elif c 1& c 2; then b 1& b 2;");
let condition: List = "c&".parse().unwrap();
let body = "b&".parse().unwrap();
let elif = ElifThen { condition, body };
assert_eq!(format!("{}", elif), "elif c& then b&");
assert_eq!(format!("{:#}", elif), "elif c& then b&");
}
#[test]
fn case_item_display() {
let patterns = vec!["foo".parse().unwrap()];
let body = "".parse::<List>().unwrap();
let item = CaseItem { patterns, body };
assert_eq!(item.to_string(), "(foo) ;;");
let patterns = vec!["bar".parse().unwrap()];
let body = "echo ok".parse::<List>().unwrap();
let item = CaseItem { patterns, body };
assert_eq!(item.to_string(), "(bar) echo ok;;");
let patterns = ["a", "b", "c"].iter().map(|s| s.parse().unwrap()).collect();
let body = "foo; bar&".parse::<List>().unwrap();
let item = CaseItem { patterns, body };
assert_eq!(item.to_string(), "(a | b | c) foo; bar&;;");
}
#[test]
fn grouping_display() {
let list = "foo".parse::<List>().unwrap();
let grouping = CompoundCommand::Grouping(list);
assert_eq!(grouping.to_string(), "{ foo; }");
}
#[test]
fn for_display_without_values() {
let name = Word::from_str("foo").unwrap();
let values = None;
let body = "echo ok".parse::<List>().unwrap();
let r#for = CompoundCommand::For { name, values, body };
assert_eq!(r#for.to_string(), "for foo do echo ok; done");
}
#[test]
fn for_display_with_empty_values() {
let name = Word::from_str("foo").unwrap();
let values = Some(vec![]);
let body = "echo ok".parse::<List>().unwrap();
let r#for = CompoundCommand::For { name, values, body };
assert_eq!(r#for.to_string(), "for foo in; do echo ok; done");
}
#[test]
fn for_display_with_some_values() {
let name = Word::from_str("V").unwrap();
let values = Some(vec![
Word::from_str("a").unwrap(),
Word::from_str("b").unwrap(),
]);
let body = "one; two&".parse::<List>().unwrap();
let r#for = CompoundCommand::For { name, values, body };
assert_eq!(r#for.to_string(), "for V in a b; do one; two& done");
}
#[test]
fn while_display() {
let condition = "true& false".parse::<List>().unwrap();
let body = "echo ok".parse::<List>().unwrap();
let r#while = CompoundCommand::While { condition, body };
assert_eq!(r#while.to_string(), "while true& false; do echo ok; done");
}
#[test]
fn until_display() {
let condition = "true& false".parse::<List>().unwrap();
let body = "echo ok".parse::<List>().unwrap();
let until = CompoundCommand::Until { condition, body };
assert_eq!(until.to_string(), "until true& false; do echo ok; done");
}
#[test]
fn if_display() {
let r#if: CompoundCommand = CompoundCommand::If {
condition: "c 1; c 2&".parse().unwrap(),
body: "b 1; b 2&".parse().unwrap(),
elifs: vec![],
r#else: None,
};
assert_eq!(r#if.to_string(), "if c 1; c 2& then b 1; b 2& fi");
let r#if: CompoundCommand = CompoundCommand::If {
condition: "c 1& c 2;".parse().unwrap(),
body: "b 1& b 2;".parse().unwrap(),
elifs: vec![ElifThen {
condition: "c 3&".parse().unwrap(),
body: "b 3&".parse().unwrap(),
}],
r#else: Some("b 4".parse().unwrap()),
};
assert_eq!(
r#if.to_string(),
"if c 1& c 2; then b 1& b 2; elif c 3& then b 3& else b 4; fi"
);
let r#if: CompoundCommand = CompoundCommand::If {
condition: "true".parse().unwrap(),
body: ":".parse().unwrap(),
elifs: vec![
ElifThen {
condition: "false".parse().unwrap(),
body: "a".parse().unwrap(),
},
ElifThen {
condition: "echo&".parse().unwrap(),
body: "b&".parse().unwrap(),
},
],
r#else: None,
};
assert_eq!(
r#if.to_string(),
"if true; then :; elif false; then a; elif echo& then b& fi"
);
}
#[test]
fn case_display() {
let subject = "foo".parse().unwrap();
let items = Vec::<CaseItem>::new();
let case = CompoundCommand::Case { subject, items };
assert_eq!(case.to_string(), "case foo in esac");
let subject = "bar".parse().unwrap();
let items = vec!["foo)".parse::<CaseItem>().unwrap()];
let case = CompoundCommand::Case { subject, items };
assert_eq!(case.to_string(), "case bar in (foo) ;; esac");
let subject = "baz".parse().unwrap();
let items = vec![
"1)".parse::<CaseItem>().unwrap(),
"(a|b|c) :&".parse().unwrap(),
];
let case = CompoundCommand::Case { subject, items };
assert_eq!(case.to_string(), "case baz in (1) ;; (a | b | c) :&;; esac");
}
#[test]
fn function_definition_display() {
let body = FullCompoundCommand {
command: "( bar )".parse::<CompoundCommand>().unwrap(),
redirs: vec![],
};
let fd = FunctionDefinition {
has_keyword: false,
name: Word::from_str("foo").unwrap(),
body: Rc::new(body),
};
assert_eq!(fd.to_string(), "foo() (bar)");
}
#[test]
fn pipeline_display() {
let mut p = Pipeline {
commands: vec![Rc::new("first".parse::<Command>().unwrap())],
negation: false,
};
assert_eq!(p.to_string(), "first");
p.negation = true;
assert_eq!(p.to_string(), "! first");
p.commands.push(Rc::new("second".parse().unwrap()));
assert_eq!(p.to_string(), "! first | second");
p.commands.push(Rc::new("third".parse().unwrap()));
p.negation = false;
assert_eq!(p.to_string(), "first | second | third");
}
#[test]
fn and_or_conversions() {
for op in &[AndOr::AndThen, AndOr::OrElse] {
let op2 = AndOr::try_from(Operator::from(*op));
assert_eq!(op2, Ok(*op));
}
}
#[test]
fn and_or_list_display() {
let p = "first".parse::<Pipeline>().unwrap();
let mut aol = AndOrList {
first: p,
rest: vec![],
};
assert_eq!(aol.to_string(), "first");
let p = "second".parse().unwrap();
aol.rest.push((AndOr::AndThen, p));
assert_eq!(aol.to_string(), "first && second");
let p = "third".parse().unwrap();
aol.rest.push((AndOr::OrElse, p));
assert_eq!(aol.to_string(), "first && second || third");
}
#[test]
fn list_display() {
let and_or = "first".parse::<AndOrList>().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: None,
};
let mut list = List(vec![item]);
assert_eq!(list.to_string(), "first");
let and_or = "second".parse().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: Some(Location::dummy("")),
};
list.0.push(item);
assert_eq!(list.to_string(), "first; second&");
let and_or = "third".parse().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: None,
};
list.0.push(item);
assert_eq!(list.to_string(), "first; second& third");
}
#[test]
fn list_display_alternate() {
let and_or = "first".parse::<AndOrList>().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: None,
};
let mut list = List(vec![item]);
assert_eq!(format!("{:#}", list), "first;");
let and_or = "second".parse().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: Some(Location::dummy("")),
};
list.0.push(item);
assert_eq!(format!("{:#}", list), "first; second&");
let and_or = "third".parse().unwrap();
let item = Item {
and_or: Rc::new(and_or),
async_flag: None,
};
list.0.push(item);
assert_eq!(format!("{:#}", list), "first; second& third;");
}
}