From 73889319c5bac9439207ffe75ffeb799722b10ac Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sun, 24 Nov 2024 13:16:33 +0900 Subject: [PATCH 1/3] Split impl fmt::Display to a separate module The syntax.rs file is getting too large, so I'm splitting the fmt::Display implementations to a separate module. --- yash-syntax/src/syntax.rs | 971 +----------------------- yash-syntax/src/syntax/impl_display.rs | 993 +++++++++++++++++++++++++ 2 files changed, 995 insertions(+), 969 deletions(-) create mode 100644 yash-syntax/src/syntax/impl_display.rs diff --git a/yash-syntax/src/syntax.rs b/yash-syntax/src/syntax.rs index 25206122..4f03a780 100644 --- a/yash-syntax/src/syntax.rs +++ b/yash-syntax/src/syntax.rs @@ -77,10 +77,8 @@ use crate::parser::lex::Keyword; use crate::parser::lex::Operator; use crate::parser::lex::TryFromOperatorError; use crate::source::Location; -use itertools::Itertools; use std::cell::OnceCell; use std::fmt; -use std::fmt::Write as _; #[cfg(unix)] use std::os::unix::io::RawFd; use std::rc::Rc; @@ -235,12 +233,6 @@ impl SpecialParam { } } -impl fmt::Display for SpecialParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.as_char().fmt(f) - } -} - /// Error that occurs when a character cannot be parsed as a special parameter /// /// This error value is returned by the `TryFrom` and `FromStr` @@ -364,12 +356,6 @@ impl From for Param { } } -impl fmt::Display for Param { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.id.fmt(f) - } -} - // TODO Consider implementing FromStr for Param /// Flag that specifies how the value is substituted in a [switch](Switch) @@ -385,19 +371,6 @@ pub enum SwitchType { 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) - } -} - /// Condition that triggers a [switch](Switch) /// /// In the lexical grammar of the shell language, a switch condition is an @@ -411,16 +384,6 @@ pub enum SwitchCondition { 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(':'), - } - } -} - /// Parameter expansion [modifier](Modifier) that conditionally substitutes the /// value being expanded /// @@ -438,12 +401,6 @@ pub struct Switch { 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(&self, w: &mut W) -> UnquoteResult { write!(w, "{}{}", self.condition, self.r#type)?; @@ -461,17 +418,6 @@ pub enum TrimSide { 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) - } -} - /// Flag that specifies pattern matching strategy in a [trim](Trim) #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TrimLength { @@ -497,17 +443,6 @@ pub struct Trim { 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(&self, w: &mut W) -> UnquoteResult { write!(w, "{}", self.side)?; @@ -550,18 +485,6 @@ pub struct BracedParam { pub location: Location, } -impl fmt::Display for BracedParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Modifier::*; - match self.modifier { - None => write!(f, "${{{}}}", self.param), - Length => write!(f, "${{#{}}}", self.param), - Switch(ref switch) => write!(f, "${{{}{}}}", self.param, switch), - Trim(ref trim) => write!(f, "${{{}{}}}", self.param, trim), - } - } -} - impl Unquote for BracedParam { fn write_unquoted(&self, w: &mut W) -> UnquoteResult { use Modifier::*; @@ -599,15 +522,6 @@ pub enum BackquoteUnit { 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(&self, w: &mut W) -> UnquoteResult { match self { @@ -670,24 +584,6 @@ pub enum TextUnit { 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 { param, .. } => write!(f, "${param}"), - 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(&self, w: &mut W) -> UnquoteResult { match self { @@ -754,12 +650,6 @@ impl Text { } } -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(&self, w: &mut W) -> UnquoteResult { self.0.write_unquoted(w) @@ -789,17 +679,6 @@ pub enum WordUnit { 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(&self, w: &mut W) -> UnquoteResult { match self { @@ -845,12 +724,6 @@ pub struct Word { 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(&self, w: &mut W) -> UnquoteResult { self.units.write_unquoted(w) @@ -881,15 +754,6 @@ pub enum Value { 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(" ")), - } - } -} - /// Assignment word #[derive(Clone, Debug, Eq, PartialEq)] pub struct Assign { @@ -903,12 +767,6 @@ pub struct Assign { pub location: Location, } -impl fmt::Display for Assign { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}={}", &self.name, &self.value) - } -} - /// Fallible conversion from a word into an assignment impl TryFrom for Assign { type Error = Word; @@ -962,12 +820,6 @@ impl From for Fd { } } -impl fmt::Display for Fd { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - /// Redirection operators /// /// This enum defines the redirection operator types except here-document and @@ -1032,12 +884,6 @@ impl From for Operator { } } -impl fmt::Display for RedirOp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Operator::from(*self).fmt(f) - } -} - /// Here-document #[derive(Clone, Debug, Eq, PartialEq)] pub struct HereDoc { @@ -1065,19 +911,6 @@ pub struct HereDoc { pub content: OnceCell, } -impl fmt::Display for HereDoc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(if self.remove_tabs { "<<-" } else { "<<" })?; - - // This space is to disambiguate `<< --` and `<<- -` - if let Some(Unquoted(Literal('-'))) = self.delimiter.units.first() { - f.write_char(' ')?; - } - - write!(f, "{}", self.delimiter) - } -} - /// Part of a redirection that defines the nature of the resulting file descriptor #[derive(Clone, Debug, Eq, PartialEq)] pub enum RedirBody { @@ -1098,15 +931,6 @@ impl RedirBody { } } -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>> From for RedirBody { fn from(t: T) -> Self { RedirBody::HereDoc(t.into()) @@ -1139,15 +963,6 @@ impl Redir { } } -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) - } -} - /// Command that involves assignments, redirections, and word expansions /// /// In the shell language syntax, a valid simple command must contain at least one of assignments, @@ -1184,24 +999,6 @@ impl SimpleCommand { } } -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); - - if !self.assigns.is_empty() || !self.first_word_is_keyword() { - write!(f, "{}", i1.chain(i2).chain(i3).format(" ")) - } else { - // If the simple command starts with an assignment or redirection, - // the first word may be a keyword which is treated as a plain word. - // In this case, we need to avoid the word being interpreted as a - // keyword by printing the assignment or redirection first. - write!(f, "{}", i3.chain(i2).format(" ")) - } - } -} - /// `elif-then` clause #[derive(Clone, Debug, Eq, PartialEq)] pub struct ElifThen { @@ -1209,17 +1006,6 @@ pub struct ElifThen { 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) - } - } -} - /// Symbol that terminates the body of a case branch and determines what to do /// after executing it #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -1268,12 +1054,6 @@ impl From for Operator { } } -impl fmt::Display for CaseContinuation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Operator::from(*self).fmt(f) - } -} - /// Branch item of a `case` compound command #[derive(Clone, Debug, Eq, PartialEq)] pub struct CaseItem { @@ -1288,18 +1068,6 @@ pub struct CaseItem { pub continuation: CaseContinuation, } -impl fmt::Display for CaseItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "({}) {}{}", - self.patterns.iter().format(" | "), - self.body, - self.continuation, - ) - } -} - /// Command that contains other commands #[derive(Clone, Debug, Eq, PartialEq)] pub enum CompoundCommand { @@ -1329,51 +1097,6 @@ pub enum CompoundCommand { // TODO [[ ]] } -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 {body:#} done") - } - While { condition, body } => write!(f, "while {condition:#} do {body:#} done"), - Until { condition, body } => write!(f, "until {condition:#} do {body:#} done"), - If { - condition, - body, - elifs, - r#else, - } => { - write!(f, "if {condition:#} then {body:#} ")?; - for elif in elifs { - write!(f, "{elif:#} ")?; - } - if let Some(r#else) = r#else { - write!(f, "else {else:#} ")?; - } - f.write_str("fi") - } - Case { subject, items } => { - write!(f, "case {subject} in ")?; - for item in items { - write!(f, "{item} ")?; - } - f.write_str("esac") - } - } - } -} - /// Compound command with redirections #[derive(Clone, Debug, Eq, PartialEq)] pub struct FullCompoundCommand { @@ -1383,14 +1106,6 @@ pub struct FullCompoundCommand { pub redirs: Vec, } -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}")) - } -} - /// Function definition command #[derive(Clone, Debug, Eq, PartialEq)] pub struct FunctionDefinition { @@ -1402,15 +1117,6 @@ pub struct FunctionDefinition { pub body: Rc, } -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) - } -} - /// Element of a pipe sequence #[derive(Clone, Debug, Eq, PartialEq)] pub enum Command { @@ -1422,16 +1128,6 @@ pub enum Command { 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), - } - } -} - /// Commands separated by `|` #[derive(Clone, Debug, Eq, PartialEq)] pub struct Pipeline { @@ -1446,15 +1142,6 @@ pub struct Pipeline { 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(" | ")) - } -} - /// Condition that decides if a [Pipeline] in an [and-or list](AndOrList) should be executed #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum AndOr { @@ -1484,15 +1171,6 @@ impl From for Operator { } } -impl fmt::Display for AndOr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AndOr::AndThen => write!(f, "&&"), - AndOr::OrElse => write!(f, "||"), - } - } -} - /// Pipelines separated by `&&` and `||` #[derive(Clone, Debug, Eq, PartialEq)] pub struct AndOrList { @@ -1500,15 +1178,6 @@ pub struct AndOrList { 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}")) - } -} - /// Element of a [List] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Item { @@ -1521,51 +1190,14 @@ pub struct Item { pub async_flag: Option, } -/// Allows conversion from Item to String. -/// -/// By default, the `;` terminator is omitted from the formatted string. -/// When the alternate flag is specified as in `{:#}`, the result is always -/// terminated by either `;` or `&`. -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(()) - } - } -} - /// Sequence of [and-or lists](AndOrList) separated by `;` or `&` /// /// It depends on context whether an empty list is a valid syntax. #[derive(Clone, Debug, Eq, PartialEq)] pub struct List(pub Vec); -/// Allows conversion from List to String. -/// -/// By default, the last `;` terminator is omitted from the formatted string. -/// When the alternate flag is specified as in `{:#}`, the result is always -/// terminated by either `;` or `&`. -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(()) - } - } -} +/// Implementations of [std::fmt::Display] for the shell language syntax types +mod impl_display; #[allow(clippy::bool_assert_comparison)] #[cfg(test)] @@ -1590,37 +1222,6 @@ mod tests { assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam)); } - #[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 { @@ -1642,37 +1243,6 @@ mod tests { 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 { @@ -1712,44 +1282,6 @@ mod tests { assert_eq!(is_quoted, false); } - #[test] - fn braced_param_display() { - let param = BracedParam { - param: Param::variable("foo"), - modifier: Modifier::None, - location: Location::dummy(""), - }; - assert_eq!(param.to_string(), "${foo}"); - - let param = BracedParam { - 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 = BracedParam { - 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 = BracedParam { - modifier: Modifier::Trim(trim), - ..param - }; - assert_eq!(param.to_string(), "${foo%baz' 'bar}"); - } - #[test] fn braced_param_unquote() { let param = BracedParam { @@ -1796,14 +1328,6 @@ mod tests { 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'); @@ -1817,43 +1341,6 @@ mod tests { 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 { - param: Param::variable("PARAM"), - 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()); @@ -1939,29 +1426,6 @@ mod tests { 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(); @@ -2003,43 +1467,6 @@ mod tests { 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(); @@ -2110,145 +1537,6 @@ mod tests { } } - #[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(), "<().unwrap(), - continuation: CaseContinuation::Break, - }; - assert_eq!(item.to_string(), "(foo) ;;"); - - let item = CaseItem { - patterns: vec!["bar".parse().unwrap()], - body: "echo ok".parse::().unwrap(), - continuation: CaseContinuation::Break, - }; - assert_eq!(item.to_string(), "(bar) echo ok;;"); - - let item = CaseItem { - patterns: ["a", "b", "c"].iter().map(|s| s.parse().unwrap()).collect(), - body: "foo; bar&".parse::().unwrap(), - continuation: CaseContinuation::Break, - }; - assert_eq!(item.to_string(), "(a | b | c) foo; bar&;;"); - - let item = CaseItem { - patterns: vec!["foo".parse().unwrap()], - body: "bar".parse::().unwrap(), - continuation: CaseContinuation::FallThrough, - }; - assert_eq!(item.to_string(), "(foo) bar;&"); - } - - #[test] - fn grouping_display() { - let list = "foo".parse::().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::().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::().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::().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::().unwrap(); - let body = "echo ok".parse::().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::().unwrap(); - let body = "echo ok".parse::().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::::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::().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::().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::().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::().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] { @@ -2452,76 +1557,4 @@ mod tests { assert_eq!(op2, Ok(*op)); } } - - #[test] - fn and_or_list_display() { - let p = "first".parse::().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::().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::().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;"); - } } diff --git a/yash-syntax/src/syntax/impl_display.rs b/yash-syntax/src/syntax/impl_display.rs new file mode 100644 index 00000000..1091b85d --- /dev/null +++ b/yash-syntax/src/syntax/impl_display.rs @@ -0,0 +1,993 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2020 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use super::*; +use itertools::Itertools as _; +use std::fmt; +use std::fmt::Write as _; + +impl fmt::Display for SpecialParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_char().fmt(f) + } +} + +impl fmt::Display for Param { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.id.fmt(f) + } +} + +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) + } +} + +impl fmt::Display for SwitchCondition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use SwitchCondition::*; + match self { + Unset => Ok(()), + UnsetOrEmpty => f.write_char(':'), + } + } +} + +impl fmt::Display for Switch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}{}", self.condition, self.r#type, self.word) + } +} + +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) + } +} + +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 fmt::Display for BracedParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Modifier::*; + match self.modifier { + None => write!(f, "${{{}}}", self.param), + Length => write!(f, "${{#{}}}", self.param), + Switch(ref switch) => write!(f, "${{{}{}}}", self.param, switch), + Trim(ref trim) => write!(f, "${{{}{}}}", self.param, trim), + } + } +} + +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 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 { param, .. } => write!(f, "${param}"), + 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 fmt::Display for Text { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.iter().try_for_each(|unit| unit.fmt(f)) + } +} + +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 fmt::Display for Word { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.units.iter().try_for_each(|unit| write!(f, "{unit}")) + } +} + +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(" ")), + } + } +} + +impl fmt::Display for Assign { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}={}", &self.name, &self.value) + } +} + +impl fmt::Display for Fd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for RedirOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Operator::from(*self).fmt(f) + } +} + +impl fmt::Display for HereDoc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(if self.remove_tabs { "<<-" } else { "<<" })?; + + // This space is to disambiguate `<< --` and `<<- -` + if let Some(Unquoted(Literal('-'))) = self.delimiter.units.first() { + f.write_char(' ')?; + } + + write!(f, "{}", self.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 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) + } +} + +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); + + if !self.assigns.is_empty() || !self.first_word_is_keyword() { + write!(f, "{}", i1.chain(i2).chain(i3).format(" ")) + } else { + // If the simple command starts with an assignment or redirection, + // the first word may be a keyword which is treated as a plain word. + // In this case, we need to avoid the word being interpreted as a + // keyword by printing the assignment or redirection first. + write!(f, "{}", i3.chain(i2).format(" ")) + } + } +} + +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) + } + } +} + +impl fmt::Display for CaseContinuation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Operator::from(*self).fmt(f) + } +} + +impl fmt::Display for CaseItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({}) {}{}", + self.patterns.iter().format(" | "), + self.body, + self.continuation, + ) + } +} + +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 {body:#} done") + } + While { condition, body } => write!(f, "while {condition:#} do {body:#} done"), + Until { condition, body } => write!(f, "until {condition:#} do {body:#} done"), + If { + condition, + body, + elifs, + r#else, + } => { + write!(f, "if {condition:#} then {body:#} ")?; + for elif in elifs { + write!(f, "{elif:#} ")?; + } + if let Some(r#else) = r#else { + write!(f, "else {else:#} ")?; + } + f.write_str("fi") + } + Case { subject, items } => { + write!(f, "case {subject} in ")?; + for item in items { + write!(f, "{item} ")?; + } + f.write_str("esac") + } + } + } +} + +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}")) + } +} + +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) + } +} + +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), + } + } +} + +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(" | ")) + } +} + +impl fmt::Display for AndOr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AndOr::AndThen => write!(f, "&&"), + AndOr::OrElse => write!(f, "||"), + } + } +} + +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}")) + } +} + +/// Allows conversion from Item to String. +/// +/// By default, the `;` terminator is omitted from the formatted string. +/// When the alternate flag is specified as in `{:#}`, the result is always +/// terminated by either `;` or `&`. +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(()) + } + } +} + +/// Allows conversion from List to String. +/// +/// By default, the last `;` terminator is omitted from the formatted string. +/// When the alternate flag is specified as in `{:#}`, the result is always +/// terminated by either `;` or `&`. +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(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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 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 braced_param_display() { + let param = BracedParam { + param: Param::variable("foo"), + modifier: Modifier::None, + location: Location::dummy(""), + }; + assert_eq!(param.to_string(), "${foo}"); + + let param = BracedParam { + 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 = BracedParam { + 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 = BracedParam { + modifier: Modifier::Trim(trim), + ..param + }; + assert_eq!(param.to_string(), "${foo%baz' 'bar}"); + } + + #[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 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 { + param: Param::variable("PARAM"), + 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 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 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 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(), "<().unwrap(), + continuation: CaseContinuation::Break, + }; + assert_eq!(item.to_string(), "(foo) ;;"); + + let item = CaseItem { + patterns: vec!["bar".parse().unwrap()], + body: "echo ok".parse::().unwrap(), + continuation: CaseContinuation::Break, + }; + assert_eq!(item.to_string(), "(bar) echo ok;;"); + + let item = CaseItem { + patterns: ["a", "b", "c"].iter().map(|s| s.parse().unwrap()).collect(), + body: "foo; bar&".parse::().unwrap(), + continuation: CaseContinuation::Break, + }; + assert_eq!(item.to_string(), "(a | b | c) foo; bar&;;"); + + let item = CaseItem { + patterns: vec!["foo".parse().unwrap()], + body: "bar".parse::().unwrap(), + continuation: CaseContinuation::FallThrough, + }; + assert_eq!(item.to_string(), "(foo) bar;&"); + } + + #[test] + fn grouping_display() { + let list = "foo".parse::().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::().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::().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::().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::().unwrap(); + let body = "echo ok".parse::().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::().unwrap(); + let body = "echo ok".parse::().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::::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::().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::().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::().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::().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_list_display() { + let p = "first".parse::().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::().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::().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;"); + } +} From f39b9b3e77d6c3e079ae55f1a54ec6a2700e7f27 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sun, 24 Nov 2024 14:10:40 +0900 Subject: [PATCH 2/3] Redefine MaybeLiteral::extend_if_literal as extend_literal The existing implementation of `MaybeLiteral::extend_if_literal` for `[T]` was wrong in that it returned `Err(result)` where `result` was modified. This is a violation of the contract of the method, which should return `result` unmodified in case of failure. To fix this, the method is redefined as `extend_literal`, which is declared to modify `result` regardless of success or failure. The new method is defined to take a mutable reference to an `Extend` object, instead of an ownership of it, because the ownership is not necessary for the method to work. --- yash-syntax/CHANGELOG.md | 6 ++++ yash-syntax/src/syntax.rs | 58 +++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/yash-syntax/CHANGELOG.md b/yash-syntax/CHANGELOG.md index 3e6231a3..78278a0e 100644 --- a/yash-syntax/CHANGELOG.md +++ b/yash-syntax/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `parser::Parser::case_item` and `syntax::CaseItem::from_str` methods now consume a trailing terminator token, if any. The terminator can be not only `;;`, but also `;&`, `;|`, or `;;&`. +- In the `syntax::MaybeLiteral` trait, the `extend_if_literal` method is + replaced with the `extend_literal` method, which now takes a mutable reference + to an `Extend` object, instead of an ownership of it. The method may + leave intermediate results in the `Extend` object if unsuccessful. + - The `syntax::NotLiteral` struct is added to represent the case where the + method is unsuccessful. ## [0.12.1] - 2024-11-10 diff --git a/yash-syntax/src/syntax.rs b/yash-syntax/src/syntax.rs index 4f03a780..54c9ab54 100644 --- a/yash-syntax/src/syntax.rs +++ b/yash-syntax/src/syntax.rs @@ -117,11 +117,19 @@ pub trait Unquote { } } +/// Error indicating that a syntax element is not a literal +/// +/// This error value is returned by [`MaybeLiteral::extend_literal`] when the +/// syntax element is not a literal. +#[derive(Debug, Error)] +#[error("not a literal")] +pub struct NotLiteral; + /// Possibly literal syntax element /// /// A syntax element is _literal_ if it is not quoted and does not contain any -/// expansions. Such an element can be expanded to a string independently of the -/// shell execution environment. +/// expansions. Such an element may be considered as a constant string, and is +/// a candidate for a keyword or identifier. /// /// ``` /// # use yash_syntax::syntax::MaybeLiteral; @@ -140,17 +148,18 @@ pub trait Unquote { /// assert_eq!(backslashed.to_string_if_literal(), None); /// ``` pub trait MaybeLiteral { - /// Checks if `self` is literal and, if so, converts to a string and appends - /// it to `result`. + /// Appends the literal representation of `self` to an extendable object. /// - /// If `self` is literal, `self` converted to a string is appended to - /// `result` and `Ok(result)` is returned. Otherwise, `result` is not - /// modified and `Err(result)` is returned. - fn extend_if_literal>(&self, result: T) -> Result; + /// If `self` is literal, the literal representation is appended to `result` + /// and `Ok(())` is returned. Otherwise, `Err(NotLiteral)` is returned and + /// `result` may contain some characters that have been appended. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral>; /// Checks if `self` is literal and, if so, converts to a string. fn to_string_if_literal(&self) -> Option { - self.extend_if_literal(String::new()).ok() + let mut result = String::new(); + self.extend_literal(&mut result).ok()?; + Some(result) } } @@ -162,9 +171,8 @@ impl Unquote for [T] { } impl MaybeLiteral for [T] { - fn extend_if_literal>(&self, result: R) -> Result { - self.iter() - .try_fold(result, |result, unit| unit.extend_if_literal(result)) + fn extend_literal>(&self, result: &mut R) -> Result<(), NotLiteral> { + self.iter().try_for_each(|item| item.extend_literal(result)) } } @@ -623,15 +631,14 @@ impl Unquote for TextUnit { } impl MaybeLiteral for TextUnit { - /// If `self` is `Literal`, appends the character to `result` and returns - /// `Ok(result)`. Otherwise, returns `Err(result)`. - fn extend_if_literal>(&self, mut result: T) -> Result { + /// If `self` is `Literal`, appends the character to `result`. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { if let Literal(c) = self { // TODO Use Extend::extend_one result.extend(std::iter::once(*c)); - Ok(result) + Ok(()) } else { - Err(result) + Err(NotLiteral) } } } @@ -657,8 +664,8 @@ impl Unquote for Text { } impl MaybeLiteral for Text { - fn extend_if_literal>(&self, result: T) -> Result { - self.0.extend_if_literal(result) + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + self.0.extend_literal(result) } } @@ -697,13 +704,12 @@ impl Unquote for WordUnit { } impl MaybeLiteral for WordUnit { - /// If `self` is `Unquoted(Literal(_))`, appends the character to `result` - /// and returns `Ok(result)`. Otherwise, returns `Err(result)`. - fn extend_if_literal>(&self, result: T) -> Result { + /// If `self` is `Unquoted(Literal(_))`, appends the character to `result`. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { if let Unquoted(inner) = self { - inner.extend_if_literal(result) + inner.extend_literal(result) } else { - Err(result) + Err(NotLiteral) } } } @@ -731,8 +737,8 @@ impl Unquote for Word { } impl MaybeLiteral for Word { - fn extend_if_literal>(&self, result: T) -> Result { - self.units.extend_if_literal(result) + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + self.units.extend_literal(result) } } From c06499031981f65c6121f06696644ac7256d7bda Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sun, 24 Nov 2024 15:27:15 +0900 Subject: [PATCH 3/3] Extract syntax type conversions into a separate module The syntax.rs file contained a lot of code about type conversions and environment-free expansions. To keep the file focused on the syntax definition, this commit moves the conversion code into a separate module, conversions.rs. --- yash-syntax/src/syntax.rs | 867 +------------------------ yash-syntax/src/syntax/conversions.rs | 882 ++++++++++++++++++++++++++ 2 files changed, 886 insertions(+), 863 deletions(-) create mode 100644 yash-syntax/src/syntax/conversions.rs diff --git a/yash-syntax/src/syntax.rs b/yash-syntax/src/syntax.rs index 54c9ab54..aafe8f25 100644 --- a/yash-syntax/src/syntax.rs +++ b/yash-syntax/src/syntax.rs @@ -78,104 +78,14 @@ use crate::parser::lex::Operator; use crate::parser::lex::TryFromOperatorError; use crate::source::Location; use std::cell::OnceCell; -use std::fmt; #[cfg(unix)] use std::os::unix::io::RawFd; use std::rc::Rc; use std::str::FromStr; -use thiserror::Error; #[cfg(not(unix))] type RawFd = i32; -/// Result of [`Unquote::write_unquoted`] -/// -/// If there is some quotes to be removed, the result will be `Ok(true)`. If no -/// quotes, `Ok(false)`. On error, `Err(Error)`. -type UnquoteResult = Result; - -/// Removing quotes from syntax without performing expansion. -/// -/// This trail will be useful only in a limited number of use cases. In the -/// normal word expansion process, quote removal is done after other kinds of -/// expansions like parameter expansion, so this trait is not used. -pub trait Unquote { - /// Converts `self` to a string with all quotes removed and writes to `w`. - fn write_unquoted(&self, w: &mut W) -> UnquoteResult; - - /// Converts `self` to a string with all quotes removed. - /// - /// Returns a tuple of a string and a bool. The string is an unquoted version - /// of `self`. The bool tells whether there is any quotes contained in - /// `self`. - 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) - } -} - -/// Error indicating that a syntax element is not a literal -/// -/// This error value is returned by [`MaybeLiteral::extend_literal`] when the -/// syntax element is not a literal. -#[derive(Debug, Error)] -#[error("not a literal")] -pub struct NotLiteral; - -/// Possibly literal syntax element -/// -/// A syntax element is _literal_ if it is not quoted and does not contain any -/// expansions. Such an element may be considered as a constant string, and is -/// a candidate for a keyword or identifier. -/// -/// ``` -/// # use yash_syntax::syntax::MaybeLiteral; -/// # use yash_syntax::syntax::Text; -/// # use yash_syntax::syntax::TextUnit::Literal; -/// let text = Text(vec![Literal('f'), Literal('o'), Literal('o')]); -/// let expanded = text.to_string_if_literal().unwrap(); -/// assert_eq!(expanded, "foo"); -/// ``` -/// -/// ``` -/// # use yash_syntax::syntax::MaybeLiteral; -/// # use yash_syntax::syntax::Text; -/// # use yash_syntax::syntax::TextUnit::Backslashed; -/// let backslashed = Text(vec![Backslashed('a')]); -/// assert_eq!(backslashed.to_string_if_literal(), None); -/// ``` -pub trait MaybeLiteral { - /// Appends the literal representation of `self` to an extendable object. - /// - /// If `self` is literal, the literal representation is appended to `result` - /// and `Ok(())` is returned. Otherwise, `Err(NotLiteral)` is returned and - /// `result` may contain some characters that have been appended. - fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral>; - - /// Checks if `self` is literal and, if so, converts to a string. - fn to_string_if_literal(&self) -> Option { - let mut result = String::new(); - self.extend_literal(&mut result).ok()?; - Some(result) - } -} - -impl Unquote for [T] { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - self.iter() - .try_fold(false, |quoted, item| Ok(quoted | item.write_unquoted(w)?)) - } -} - -impl MaybeLiteral for [T] { - fn extend_literal>(&self, result: &mut R) -> Result<(), NotLiteral> { - self.iter().try_for_each(|item| item.extend_literal(result)) - } -} - /// Special parameter /// /// This enum value identifies a special parameter in the shell language. @@ -203,73 +113,6 @@ pub enum SpecialParam { Zero, } -impl SpecialParam { - /// Returns the character representing the special parameter. - #[must_use] - pub const fn as_char(self) -> char { - use SpecialParam::*; - match self { - At => '@', - Asterisk => '*', - Number => '#', - Question => '?', - Hyphen => '-', - Dollar => '$', - Exclamation => '!', - Zero => '0', - } - } - - /// Returns the special parameter that corresponds to the given character. - /// - /// If the character does not represent any special parameter, `None` is - /// returned. - #[must_use] - pub const fn from_char(c: char) -> Option { - use SpecialParam::*; - match c { - '@' => Some(At), - '*' => Some(Asterisk), - '#' => Some(Number), - '?' => Some(Question), - '-' => Some(Hyphen), - '$' => Some(Dollar), - '!' => Some(Exclamation), - '0' => Some(Zero), - _ => None, - } - } -} - -/// Error that occurs when a character cannot be parsed as a special parameter -/// -/// This error value is returned by the `TryFrom` and `FromStr` -/// implementations for [`SpecialParam`]. -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error("not a special parameter")] -pub struct NotSpecialParam; - -impl TryFrom for SpecialParam { - type Error = NotSpecialParam; - fn try_from(c: char) -> Result { - SpecialParam::from_char(c).ok_or(NotSpecialParam) - } -} - -impl FromStr for SpecialParam { - type Err = NotSpecialParam; - fn from_str(s: &str) -> Result { - // If `s` contains a single character and nothing else, parse it as a - // special parameter. - let mut chars = s.chars(); - chars - .next() - .filter(|_| chars.as_str().is_empty()) - .and_then(SpecialParam::from_char) - .ok_or(NotSpecialParam) - } -} - /// Type of a parameter /// /// This enum distinguishes three types of [parameters](Param): named, special and @@ -298,12 +141,6 @@ pub enum ParamType { Positional(usize), } -impl From for ParamType { - fn from(special: SpecialParam) -> ParamType { - ParamType::Special(special) - } -} - /// Parameter /// /// A parameter is an identifier that appears in a parameter expansion @@ -330,40 +167,6 @@ pub struct Param { pub r#type: ParamType, } -impl Param { - /// Constructs a `Param` value representing a named parameter. - /// - /// This function assumes that the argument is a valid name for a variable. - /// The returned `Param` value will have the `Variable` type regardless of - /// the argument. - #[must_use] - pub fn variable>(id: I) -> Param { - let id = id.into(); - let r#type = ParamType::Variable; - Param { id, r#type } - } -} - -/// Constructs a `Param` value representing a special parameter. -impl From for Param { - fn from(special: SpecialParam) -> Param { - Param { - id: special.to_string(), - r#type: special.into(), - } - } -} - -/// Constructs a `Param` value from a positional parameter index. -impl From for Param { - fn from(index: usize) -> Param { - Param { - id: index.to_string(), - r#type: ParamType::Positional(index), - } - } -} - // TODO Consider implementing FromStr for Param /// Flag that specifies how the value is substituted in a [switch](Switch) @@ -409,13 +212,6 @@ pub struct Switch { pub word: Word, } -impl Unquote for Switch { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - write!(w, "{}{}", self.condition, self.r#type)?; - self.word.write_unquoted(w) - } -} - /// Flag that specifies which side of the expanded value is removed in a /// [trim](Trim) #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -451,17 +247,6 @@ pub struct Trim { pub pattern: Word, } -impl Unquote for Trim { - fn write_unquoted(&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) - } -} - /// Attribute that modifies a parameter expansion #[derive(Clone, Debug, Eq, PartialEq)] pub enum Modifier { @@ -493,34 +278,6 @@ pub struct BracedParam { pub location: Location, } -impl Unquote for BracedParam { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - use Modifier::*; - match self.modifier { - None => { - write!(w, "${{{}}}", self.param)?; - Ok(false) - } - Length => { - write!(w, "${{#{}}}", self.param)?; - Ok(false) - } - Switch(ref switch) => { - write!(w, "${{{}", self.param)?; - let quoted = switch.write_unquoted(w)?; - w.write_char('}')?; - Ok(quoted) - } - Trim(ref trim) => { - write!(w, "${{{}", self.param)?; - let quoted = trim.write_unquoted(w)?; - w.write_char('}')?; - Ok(quoted) - } - } - } -} - /// Element of [`TextUnit::Backquote`] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BackquoteUnit { @@ -530,21 +287,6 @@ pub enum BackquoteUnit { Backslashed(char), } -impl Unquote for BackquoteUnit { - fn write_unquoted(&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) - } - } - } -} - /// Element of a [Text], i.e., something that can be expanded #[derive(Clone, Debug, Eq, PartialEq)] pub enum TextUnit { @@ -592,57 +334,6 @@ pub enum TextUnit { pub use TextUnit::*; -impl Unquote for TextUnit { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - match self { - Literal(c) => { - w.write_char(*c)?; - Ok(false) - } - Backslashed(c) => { - w.write_char(*c)?; - Ok(true) - } - RawParam { param, .. } => { - write!(w, "${param}")?; - Ok(false) - } - BracedParam(param) => param.write_unquoted(w), - // We don't remove quotes contained in the commands in command - // substitutions. Existing shells disagree with each other. - 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 { - /// If `self` is `Literal`, appends the character to `result`. - fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { - if let Literal(c) = self { - // TODO Use Extend::extend_one - result.extend(std::iter::once(*c)); - Ok(()) - } else { - Err(NotLiteral) - } - } -} - /// String that may contain some expansions /// /// A text is a sequence of [text unit](TextUnit)s, which may contain some kinds @@ -650,25 +341,6 @@ impl MaybeLiteral for TextUnit { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Text(pub Vec); -impl Text { - /// Creates a text from an iterator of literal chars. - pub fn from_literal_chars>(i: I) -> Text { - Text(i.into_iter().map(Literal).collect()) - } -} - -impl Unquote for Text { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - self.0.write_unquoted(w) - } -} - -impl MaybeLiteral for Text { - fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { - self.0.extend_literal(result) - } -} - /// Element of a [Word], i.e., text with quotes and tilde expansion #[derive(Clone, Debug, Eq, PartialEq)] pub enum WordUnit { @@ -686,34 +358,6 @@ pub enum WordUnit { pub use WordUnit::*; -impl Unquote for WordUnit { - fn write_unquoted(&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 { - /// If `self` is `Unquoted(Literal(_))`, appends the character to `result`. - fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { - if let Unquoted(inner) = self { - inner.extend_literal(result) - } else { - Err(NotLiteral) - } - } -} - /// Token that may involve expansions and quotes /// /// A word is a sequence of [word unit](WordUnit)s. It depends on context whether @@ -730,18 +374,6 @@ pub struct Word { pub location: Location, } -impl Unquote for Word { - fn write_unquoted(&self, w: &mut W) -> UnquoteResult { - self.units.write_unquoted(w) - } -} - -impl MaybeLiteral for Word { - fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { - self.units.extend_literal(result) - } -} - /// Value of an [assignment](Assign) #[derive(Clone, Debug, Eq, PartialEq)] pub enum Value { @@ -773,37 +405,6 @@ pub struct Assign { pub location: Location, } -/// Fallible conversion from a word into an assignment -impl TryFrom for Assign { - type Error = Word; - /// Converts a word into an assignment. - /// - /// For a successful conversion, the word must be of the form `name=value`, - /// where `name` is a non-empty [literal](Word::to_string_if_literal) word, - /// `=` is an unquoted equal sign, and `value` is a word. If the input word - /// does not match this syntax, it is returned intact in `Err`. - fn try_from(mut word: Word) -> Result { - 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) - } -} - /// File descriptor /// /// This is the `newtype` pattern applied to [`RawFd`], which is merely a type @@ -820,12 +421,6 @@ impl Fd { pub const STDERR: Fd = Fd(2); } -impl From for Fd { - fn from(raw_fd: RawFd) -> Fd { - Fd(raw_fd) - } -} - /// Redirection operators /// /// This enum defines the redirection operator types except here-document and @@ -852,44 +447,6 @@ pub enum RedirOp { String, } -impl TryFrom for RedirOp { - type Error = TryFromOperatorError; - fn try_from(op: Operator) -> Result { - 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(TryFromOperatorError {}), - } - } -} - -impl From 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, - } - } -} - /// Here-document #[derive(Clone, Debug, Eq, PartialEq)] pub struct HereDoc { @@ -937,12 +494,6 @@ impl RedirBody { } } -impl>> From for RedirBody { - fn from(t: T) -> Self { - RedirBody::HereDoc(t.into()) - } -} - /// Redirection #[derive(Clone, Debug, Eq, PartialEq)] pub struct Redir { @@ -1025,41 +576,6 @@ pub enum CaseContinuation { Continue, } -impl TryFrom for CaseContinuation { - type Error = TryFromOperatorError; - - /// Converts an operator into a case continuation. - /// - /// The `SemicolonBar` and `SemicolonSemicolonAnd` operators are converted - /// into `Continue`; you cannot distinguish between the two from the return - /// value. - fn try_from(op: Operator) -> Result { - use CaseContinuation::*; - use Operator::*; - match op { - SemicolonSemicolon => Ok(Break), - SemicolonAnd => Ok(FallThrough), - SemicolonBar | SemicolonSemicolonAnd => Ok(Continue), - _ => Err(TryFromOperatorError {}), - } - } -} - -impl From for Operator { - /// Converts a case continuation into an operator. - /// - /// The `Continue` variant is converted into `SemicolonBar`. - fn from(cc: CaseContinuation) -> Operator { - use CaseContinuation::*; - use Operator::*; - match cc { - Break => SemicolonSemicolon, - FallThrough => SemicolonAnd, - Continue => SemicolonBar, - } - } -} - /// Branch item of a `case` compound command #[derive(Clone, Debug, Eq, PartialEq)] pub struct CaseItem { @@ -1157,26 +673,6 @@ pub enum AndOr { OrElse, } -impl TryFrom for AndOr { - type Error = TryFromOperatorError; - fn try_from(op: Operator) -> Result { - match op { - Operator::AndAnd => Ok(AndOr::AndThen), - Operator::BarBar => Ok(AndOr::OrElse), - _ => Err(TryFromOperatorError {}), - } - } -} - -impl From for Operator { - fn from(op: AndOr) -> Operator { - match op { - AndOr::AndThen => Operator::AndAnd, - AndOr::OrElse => Operator::BarBar, - } - } -} - /// Pipelines separated by `&&` and `||` #[derive(Clone, Debug, Eq, PartialEq)] pub struct AndOrList { @@ -1202,365 +698,10 @@ pub struct Item { #[derive(Clone, Debug, Eq, PartialEq)] pub struct List(pub Vec); +/// Definitions and implementations of the [Unquote] and [MaybeLiteral] traits, +/// and other conversions between types +mod conversions; /// Implementations of [std::fmt::Display] for the shell language syntax types mod impl_display; -#[allow(clippy::bool_assert_comparison)] -#[cfg(test)] -mod tests { - use super::*; - use assert_matches::assert_matches; - - #[test] - fn special_param_from_str() { - assert_eq!("@".parse(), Ok(SpecialParam::At)); - assert_eq!("*".parse(), Ok(SpecialParam::Asterisk)); - assert_eq!("#".parse(), Ok(SpecialParam::Number)); - assert_eq!("?".parse(), Ok(SpecialParam::Question)); - assert_eq!("-".parse(), Ok(SpecialParam::Hyphen)); - assert_eq!("$".parse(), Ok(SpecialParam::Dollar)); - assert_eq!("!".parse(), Ok(SpecialParam::Exclamation)); - assert_eq!("0".parse(), Ok(SpecialParam::Zero)); - - assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam)); - assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam)); - assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam)); - assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam)); - } - - #[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_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_unquote() { - let param = BracedParam { - param: Param::variable("foo"), - modifier: Modifier::None, - location: Location::dummy(""), - }; - let (unquoted, is_quoted) = param.unquote(); - assert_eq!(unquoted, "${foo}"); - assert_eq!(is_quoted, false); - - let param = BracedParam { - 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 = BracedParam { - 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 = BracedParam { - 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_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_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 { - param: Param::variable("X"), - 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_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 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 case_continuation_conversions() { - use CaseContinuation::*; - for cc in &[Break, FallThrough, Continue] { - let cc2 = CaseContinuation::try_from(Operator::from(*cc)); - assert_eq!(cc2, Ok(*cc)); - } - assert_eq!( - CaseContinuation::try_from(Operator::SemicolonSemicolonAnd), - Ok(Continue) - ); - } - - #[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)); - } - } -} +pub use conversions::{MaybeLiteral, NotLiteral, NotSpecialParam, Unquote}; diff --git a/yash-syntax/src/syntax/conversions.rs b/yash-syntax/src/syntax/conversions.rs new file mode 100644 index 00000000..4232201e --- /dev/null +++ b/yash-syntax/src/syntax/conversions.rs @@ -0,0 +1,882 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2020 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use super::*; +use std::fmt; +use thiserror::Error; + +/// Result of [`Unquote::write_unquoted`] +/// +/// If there is some quotes to be removed, the result will be `Ok(true)`. If no +/// quotes, `Ok(false)`. On error, `Err(Error)`. +type UnquoteResult = Result; + +/// Removing quotes from syntax without performing expansion. +/// +/// This trail will be useful only in a limited number of use cases. In the +/// normal word expansion process, quote removal is done after other kinds of +/// expansions like parameter expansion, so this trait is not used. +pub trait Unquote { + /// Converts `self` to a string with all quotes removed and writes to `w`. + fn write_unquoted(&self, w: &mut W) -> UnquoteResult; + + /// Converts `self` to a string with all quotes removed. + /// + /// Returns a tuple of a string and a bool. The string is an unquoted version + /// of `self`. The bool tells whether there is any quotes contained in + /// `self`. + 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) + } +} + +/// Error indicating that a syntax element is not a literal +/// +/// This error value is returned by [`MaybeLiteral::extend_literal`] when the +/// syntax element is not a literal. +#[derive(Debug, Error)] +#[error("not a literal")] +pub struct NotLiteral; + +/// Possibly literal syntax element +/// +/// A syntax element is _literal_ if it is not quoted and does not contain any +/// expansions. Such an element may be considered as a constant string, and is +/// a candidate for a keyword or identifier. +/// +/// ``` +/// # use yash_syntax::syntax::MaybeLiteral; +/// # use yash_syntax::syntax::Text; +/// # use yash_syntax::syntax::TextUnit::Literal; +/// let text = Text(vec![Literal('f'), Literal('o'), Literal('o')]); +/// let expanded = text.to_string_if_literal().unwrap(); +/// assert_eq!(expanded, "foo"); +/// ``` +/// +/// ``` +/// # use yash_syntax::syntax::MaybeLiteral; +/// # use yash_syntax::syntax::Text; +/// # use yash_syntax::syntax::TextUnit::Backslashed; +/// let backslashed = Text(vec![Backslashed('a')]); +/// assert_eq!(backslashed.to_string_if_literal(), None); +/// ``` +pub trait MaybeLiteral { + /// Appends the literal representation of `self` to an extendable object. + /// + /// If `self` is literal, the literal representation is appended to `result` + /// and `Ok(())` is returned. Otherwise, `Err(NotLiteral)` is returned and + /// `result` may contain some characters that have been appended. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral>; + + /// Checks if `self` is literal and, if so, converts to a string. + fn to_string_if_literal(&self) -> Option { + let mut result = String::new(); + self.extend_literal(&mut result).ok()?; + Some(result) + } +} + +impl Unquote for [T] { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + self.iter() + .try_fold(false, |quoted, item| Ok(quoted | item.write_unquoted(w)?)) + } +} + +impl MaybeLiteral for [T] { + fn extend_literal>(&self, result: &mut R) -> Result<(), NotLiteral> { + self.iter().try_for_each(|item| item.extend_literal(result)) + } +} + +impl SpecialParam { + /// Returns the character representing the special parameter. + #[must_use] + pub const fn as_char(self) -> char { + use SpecialParam::*; + match self { + At => '@', + Asterisk => '*', + Number => '#', + Question => '?', + Hyphen => '-', + Dollar => '$', + Exclamation => '!', + Zero => '0', + } + } + + /// Returns the special parameter that corresponds to the given character. + /// + /// If the character does not represent any special parameter, `None` is + /// returned. + #[must_use] + pub const fn from_char(c: char) -> Option { + use SpecialParam::*; + match c { + '@' => Some(At), + '*' => Some(Asterisk), + '#' => Some(Number), + '?' => Some(Question), + '-' => Some(Hyphen), + '$' => Some(Dollar), + '!' => Some(Exclamation), + '0' => Some(Zero), + _ => None, + } + } +} + +/// Error that occurs when a character cannot be parsed as a special parameter +/// +/// This error value is returned by the `TryFrom` and `FromStr` +/// implementations for [`SpecialParam`]. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[error("not a special parameter")] +pub struct NotSpecialParam; + +impl TryFrom for SpecialParam { + type Error = NotSpecialParam; + fn try_from(c: char) -> Result { + SpecialParam::from_char(c).ok_or(NotSpecialParam) + } +} + +impl FromStr for SpecialParam { + type Err = NotSpecialParam; + fn from_str(s: &str) -> Result { + // If `s` contains a single character and nothing else, parse it as a + // special parameter. + let mut chars = s.chars(); + chars + .next() + .filter(|_| chars.as_str().is_empty()) + .and_then(SpecialParam::from_char) + .ok_or(NotSpecialParam) + } +} + +impl From for ParamType { + fn from(special: SpecialParam) -> ParamType { + ParamType::Special(special) + } +} + +impl Param { + /// Constructs a `Param` value representing a named parameter. + /// + /// This function assumes that the argument is a valid name for a variable. + /// The returned `Param` value will have the `Variable` type regardless of + /// the argument. + #[must_use] + pub fn variable>(id: I) -> Param { + let id = id.into(); + let r#type = ParamType::Variable; + Param { id, r#type } + } +} + +/// Constructs a `Param` value representing a special parameter. +impl From for Param { + fn from(special: SpecialParam) -> Param { + Param { + id: special.to_string(), + r#type: special.into(), + } + } +} + +/// Constructs a `Param` value from a positional parameter index. +impl From for Param { + fn from(index: usize) -> Param { + Param { + id: index.to_string(), + r#type: ParamType::Positional(index), + } + } +} + +impl Unquote for Switch { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + write!(w, "{}{}", self.condition, self.r#type)?; + self.word.write_unquoted(w) + } +} + +impl Unquote for Trim { + fn write_unquoted(&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) + } +} + +impl Unquote for BracedParam { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + use Modifier::*; + match self.modifier { + None => { + write!(w, "${{{}}}", self.param)?; + Ok(false) + } + Length => { + write!(w, "${{#{}}}", self.param)?; + Ok(false) + } + Switch(ref switch) => { + write!(w, "${{{}", self.param)?; + let quoted = switch.write_unquoted(w)?; + w.write_char('}')?; + Ok(quoted) + } + Trim(ref trim) => { + write!(w, "${{{}", self.param)?; + let quoted = trim.write_unquoted(w)?; + w.write_char('}')?; + Ok(quoted) + } + } + } +} + +impl Unquote for BackquoteUnit { + fn write_unquoted(&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) + } + } + } +} + +impl Unquote for TextUnit { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + match self { + Literal(c) => { + w.write_char(*c)?; + Ok(false) + } + Backslashed(c) => { + w.write_char(*c)?; + Ok(true) + } + RawParam { param, .. } => { + write!(w, "${param}")?; + Ok(false) + } + BracedParam(param) => param.write_unquoted(w), + // We don't remove quotes contained in the commands in command + // substitutions. Existing shells disagree with each other. + 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 { + /// If `self` is `Literal`, appends the character to `result`. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + if let Literal(c) = self { + // TODO Use Extend::extend_one + result.extend(std::iter::once(*c)); + Ok(()) + } else { + Err(NotLiteral) + } + } +} + +impl Text { + /// Creates a text from an iterator of literal chars. + #[must_use] + pub fn from_literal_chars>(i: I) -> Text { + Text(i.into_iter().map(Literal).collect()) + } +} + +impl Unquote for Text { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + self.0.write_unquoted(w) + } +} + +impl MaybeLiteral for Text { + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + self.0.extend_literal(result) + } +} + +impl Unquote for WordUnit { + fn write_unquoted(&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 { + /// If `self` is `Unquoted(Literal(_))`, appends the character to `result`. + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + if let Unquoted(inner) = self { + inner.extend_literal(result) + } else { + Err(NotLiteral) + } + } +} + +impl Unquote for Word { + fn write_unquoted(&self, w: &mut W) -> UnquoteResult { + self.units.write_unquoted(w) + } +} + +impl MaybeLiteral for Word { + fn extend_literal>(&self, result: &mut T) -> Result<(), NotLiteral> { + self.units.extend_literal(result) + } +} + +/// Fallible conversion from a word into an assignment +impl TryFrom for Assign { + type Error = Word; + /// Converts a word into an assignment. + /// + /// For a successful conversion, the word must be of the form `name=value`, + /// where `name` is a non-empty [literal](Word::to_string_if_literal) word, + /// `=` is an unquoted equal sign, and `value` is a word. If the input word + /// does not match this syntax, it is returned intact in `Err`. + fn try_from(mut word: Word) -> Result { + 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) + } +} + +impl From for Fd { + fn from(raw_fd: RawFd) -> Fd { + Fd(raw_fd) + } +} + +impl TryFrom for RedirOp { + type Error = TryFromOperatorError; + fn try_from(op: Operator) -> Result { + 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(TryFromOperatorError {}), + } + } +} + +impl From 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>> From for RedirBody { + fn from(t: T) -> Self { + RedirBody::HereDoc(t.into()) + } +} + +impl TryFrom for CaseContinuation { + type Error = TryFromOperatorError; + + /// Converts an operator into a case continuation. + /// + /// The `SemicolonBar` and `SemicolonSemicolonAnd` operators are converted + /// into `Continue`; you cannot distinguish between the two from the return + /// value. + fn try_from(op: Operator) -> Result { + use CaseContinuation::*; + use Operator::*; + match op { + SemicolonSemicolon => Ok(Break), + SemicolonAnd => Ok(FallThrough), + SemicolonBar | SemicolonSemicolonAnd => Ok(Continue), + _ => Err(TryFromOperatorError {}), + } + } +} + +impl From for Operator { + /// Converts a case continuation into an operator. + /// + /// The `Continue` variant is converted into `SemicolonBar`. + fn from(cc: CaseContinuation) -> Operator { + use CaseContinuation::*; + use Operator::*; + match cc { + Break => SemicolonSemicolon, + FallThrough => SemicolonAnd, + Continue => SemicolonBar, + } + } +} + +impl TryFrom for AndOr { + type Error = TryFromOperatorError; + fn try_from(op: Operator) -> Result { + match op { + Operator::AndAnd => Ok(AndOr::AndThen), + Operator::BarBar => Ok(AndOr::OrElse), + _ => Err(TryFromOperatorError {}), + } + } +} + +impl From for Operator { + fn from(op: AndOr) -> Operator { + match op { + AndOr::AndThen => Operator::AndAnd, + AndOr::OrElse => Operator::BarBar, + } + } +} + +#[allow(clippy::bool_assert_comparison)] +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + + #[test] + fn special_param_from_str() { + assert_eq!("@".parse(), Ok(SpecialParam::At)); + assert_eq!("*".parse(), Ok(SpecialParam::Asterisk)); + assert_eq!("#".parse(), Ok(SpecialParam::Number)); + assert_eq!("?".parse(), Ok(SpecialParam::Question)); + assert_eq!("-".parse(), Ok(SpecialParam::Hyphen)); + assert_eq!("$".parse(), Ok(SpecialParam::Dollar)); + assert_eq!("!".parse(), Ok(SpecialParam::Exclamation)); + assert_eq!("0".parse(), Ok(SpecialParam::Zero)); + + assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam)); + assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam)); + assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam)); + assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam)); + } + + #[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_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_unquote() { + let param = BracedParam { + param: Param::variable("foo"), + modifier: Modifier::None, + location: Location::dummy(""), + }; + let (unquoted, is_quoted) = param.unquote(); + assert_eq!(unquoted, "${foo}"); + assert_eq!(is_quoted, false); + + let param = BracedParam { + 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 = BracedParam { + 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 = BracedParam { + 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_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_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 { + param: Param::variable("X"), + 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_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 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 case_continuation_conversions() { + use CaseContinuation::*; + for cc in &[Break, FallThrough, Continue] { + let cc2 = CaseContinuation::try_from(Operator::from(*cc)); + assert_eq!(cc2, Ok(*cc)); + } + assert_eq!( + CaseContinuation::try_from(Operator::SemicolonSemicolonAnd), + Ok(Continue) + ); + } + + #[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)); + } + } +}