Skip to main content

yash_syntax/syntax/
conversions.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2020 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18use std::fmt;
19use thiserror::Error;
20
21/// Result of [`Unquote::write_unquoted`]
22///
23/// If there is some quotes to be removed, the result will be `Ok(true)`. If no
24/// quotes, `Ok(false)`. On error, `Err(Error)`.
25type UnquoteResult = Result<bool, fmt::Error>;
26
27/// Removing quotes from syntax without performing expansion.
28///
29/// This trail will be useful only in a limited number of use cases. In the
30/// normal word expansion process, quote removal is done after other kinds of
31/// expansions like parameter expansion, so this trait is not used.
32pub trait Unquote {
33    /// Converts `self` to a string with all quotes removed and writes to `w`.
34    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult;
35
36    /// Converts `self` to a string with all quotes removed.
37    ///
38    /// Returns a tuple of a string and a bool. The string is an unquoted version
39    /// of `self`. The bool tells whether there is any quotes contained in
40    /// `self`.
41    fn unquote(&self) -> (String, bool) {
42        let mut unquoted = String::new();
43        let is_quoted = self
44            .write_unquoted(&mut unquoted)
45            .expect("`write_unquoted` should not fail when writing to a `String`");
46        (unquoted, is_quoted)
47    }
48}
49
50/// Error indicating that a syntax element is not a literal
51///
52/// This error value is returned by [`MaybeLiteral::extend_literal`] when the
53/// syntax element is not a literal.
54#[derive(Debug, Error)]
55#[error("not a literal")]
56pub struct NotLiteral;
57
58/// Possibly literal syntax element
59///
60/// A syntax element is _literal_ if it is not quoted and does not contain any
61/// expansions. Such an element may be considered as a constant string, and is
62/// a candidate for a keyword or identifier.
63///
64/// ```
65/// # use yash_syntax::syntax::MaybeLiteral;
66/// # use yash_syntax::syntax::Text;
67/// # use yash_syntax::syntax::TextUnit::Literal;
68/// let text = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
69/// let expanded = text.to_string_if_literal().unwrap();
70/// assert_eq!(expanded, "foo");
71/// ```
72///
73/// ```
74/// # use yash_syntax::syntax::MaybeLiteral;
75/// # use yash_syntax::syntax::Text;
76/// # use yash_syntax::syntax::TextUnit::Backslashed;
77/// let backslashed = Text(vec![Backslashed('a')]);
78/// assert_eq!(backslashed.to_string_if_literal(), None);
79/// ```
80pub trait MaybeLiteral {
81    /// Appends the literal representation of `self` to an extendable object.
82    ///
83    /// If `self` is literal, the literal representation is appended to `result`
84    /// and `Ok(())` is returned. Otherwise, `Err(NotLiteral)` is returned and
85    /// `result` may contain some characters that have been appended.
86    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral>;
87
88    /// Checks if `self` is literal and, if so, converts to a string.
89    fn to_string_if_literal(&self) -> Option<String> {
90        let mut result = String::new();
91        self.extend_literal(&mut result).ok()?;
92        Some(result)
93    }
94}
95
96impl<T: Unquote> Unquote for [T] {
97    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
98        self.iter()
99            .try_fold(false, |quoted, item| Ok(quoted | item.write_unquoted(w)?))
100    }
101}
102
103impl<T: MaybeLiteral> MaybeLiteral for [T] {
104    fn extend_literal<R: Extend<char>>(&self, result: &mut R) -> Result<(), NotLiteral> {
105        self.iter().try_for_each(|item| item.extend_literal(result))
106    }
107}
108
109impl SpecialParam {
110    /// Returns the character representing the special parameter.
111    #[must_use]
112    pub const fn as_char(self) -> char {
113        use SpecialParam::*;
114        match self {
115            At => '@',
116            Asterisk => '*',
117            Number => '#',
118            Question => '?',
119            Hyphen => '-',
120            Dollar => '$',
121            Exclamation => '!',
122            Zero => '0',
123        }
124    }
125
126    /// Returns the special parameter that corresponds to the given character.
127    ///
128    /// If the character does not represent any special parameter, `None` is
129    /// returned.
130    #[must_use]
131    pub const fn from_char(c: char) -> Option<SpecialParam> {
132        use SpecialParam::*;
133        match c {
134            '@' => Some(At),
135            '*' => Some(Asterisk),
136            '#' => Some(Number),
137            '?' => Some(Question),
138            '-' => Some(Hyphen),
139            '$' => Some(Dollar),
140            '!' => Some(Exclamation),
141            '0' => Some(Zero),
142            _ => None,
143        }
144    }
145}
146
147/// Error that occurs when a character cannot be parsed as a special parameter
148///
149/// This error value is returned by the `TryFrom<char>` and `FromStr`
150/// implementations for [`SpecialParam`].
151#[derive(Clone, Debug, Eq, Error, PartialEq)]
152#[error("not a special parameter")]
153pub struct NotSpecialParam;
154
155impl TryFrom<char> for SpecialParam {
156    type Error = NotSpecialParam;
157    fn try_from(c: char) -> Result<SpecialParam, NotSpecialParam> {
158        SpecialParam::from_char(c).ok_or(NotSpecialParam)
159    }
160}
161
162impl FromStr for SpecialParam {
163    type Err = NotSpecialParam;
164    fn from_str(s: &str) -> Result<SpecialParam, NotSpecialParam> {
165        // If `s` contains a single character and nothing else, parse it as a
166        // special parameter.
167        let mut chars = s.chars();
168        chars
169            .next()
170            .filter(|_| chars.as_str().is_empty())
171            .and_then(SpecialParam::from_char)
172            .ok_or(NotSpecialParam)
173    }
174}
175
176impl From<SpecialParam> for ParamType {
177    fn from(special: SpecialParam) -> ParamType {
178        ParamType::Special(special)
179    }
180}
181
182impl Param {
183    /// Constructs a `Param` value representing a named parameter.
184    ///
185    /// This function assumes that the argument is a valid name for a variable.
186    /// The returned `Param` value will have the `Variable` type regardless of
187    /// the argument.
188    #[must_use]
189    pub fn variable<I: Into<String>>(id: I) -> Param {
190        let id = id.into();
191        let r#type = ParamType::Variable;
192        Param { id, r#type }
193    }
194}
195
196/// Constructs a `Param` value representing a special parameter.
197impl From<SpecialParam> for Param {
198    fn from(special: SpecialParam) -> Param {
199        Param {
200            id: special.to_string(),
201            r#type: special.into(),
202        }
203    }
204}
205
206/// Constructs a `Param` value from a positional parameter index.
207impl From<usize> for Param {
208    fn from(index: usize) -> Param {
209        Param {
210            id: index.to_string(),
211            r#type: ParamType::Positional(index),
212        }
213    }
214}
215
216impl Unquote for Switch {
217    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
218        write!(w, "{}{}", self.condition, self.action)?;
219        self.word.write_unquoted(w)
220    }
221}
222
223impl Unquote for Trim {
224    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
225        write!(w, "{}", self.side)?;
226        match self.length {
227            TrimLength::Shortest => (),
228            TrimLength::Longest => write!(w, "{}", self.side)?,
229        }
230        self.pattern.write_unquoted(w)
231    }
232}
233
234impl Unquote for BracedParam {
235    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
236        use Modifier::*;
237        match self.modifier {
238            None => {
239                write!(w, "${{{}}}", self.param)?;
240                Ok(false)
241            }
242            Length => {
243                write!(w, "${{#{}}}", self.param)?;
244                Ok(false)
245            }
246            Switch(ref switch) => {
247                write!(w, "${{{}", self.param)?;
248                let quoted = switch.write_unquoted(w)?;
249                w.write_char('}')?;
250                Ok(quoted)
251            }
252            Trim(ref trim) => {
253                write!(w, "${{{}", self.param)?;
254                let quoted = trim.write_unquoted(w)?;
255                w.write_char('}')?;
256                Ok(quoted)
257            }
258        }
259    }
260}
261
262impl Unquote for BackquoteUnit {
263    fn write_unquoted<W: std::fmt::Write>(&self, w: &mut W) -> UnquoteResult {
264        match self {
265            BackquoteUnit::Literal(c) => {
266                w.write_char(*c)?;
267                Ok(false)
268            }
269            BackquoteUnit::Backslashed(c) => {
270                w.write_char(*c)?;
271                Ok(true)
272            }
273        }
274    }
275}
276
277impl Unquote for TextUnit {
278    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
279        match self {
280            Literal(c) => {
281                w.write_char(*c)?;
282                Ok(false)
283            }
284            Backslashed(c) => {
285                w.write_char(*c)?;
286                Ok(true)
287            }
288            RawParam { param, .. } => {
289                write!(w, "${param}")?;
290                Ok(false)
291            }
292            BracedParam(param) => param.write_unquoted(w),
293            // We don't remove quotes contained in the commands in command
294            // substitutions. Existing shells disagree with each other.
295            CommandSubst { content, .. } => {
296                write!(w, "$({content})")?;
297                Ok(false)
298            }
299            Backquote { content, .. } => {
300                w.write_char('`')?;
301                let quoted = content.write_unquoted(w)?;
302                w.write_char('`')?;
303                Ok(quoted)
304            }
305            Arith { content, .. } => {
306                w.write_str("$((")?;
307                let quoted = content.write_unquoted(w)?;
308                w.write_str("))")?;
309                Ok(quoted)
310            }
311        }
312    }
313}
314
315impl MaybeLiteral for TextUnit {
316    /// If `self` is `Literal`, appends the character to `result`.
317    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
318        if let Literal(c) = self {
319            // TODO Use Extend::extend_one
320            result.extend(std::iter::once(*c));
321            Ok(())
322        } else {
323            Err(NotLiteral)
324        }
325    }
326}
327
328impl Text {
329    /// Creates a text from an iterator of literal chars.
330    #[must_use]
331    pub fn from_literal_chars<I: IntoIterator<Item = char>>(i: I) -> Text {
332        Text(i.into_iter().map(Literal).collect())
333    }
334}
335
336impl Unquote for Text {
337    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
338        self.0.write_unquoted(w)
339    }
340}
341
342impl MaybeLiteral for Text {
343    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
344        self.0.extend_literal(result)
345    }
346}
347
348/// Converts an escape unit into the string represented by the escape sequence.
349///
350/// Produces an empty string if the escape unit does not represent a valid
351/// Unicode scalar value.
352impl Unquote for EscapeUnit {
353    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
354        match self {
355            Self::Literal(c) => {
356                w.write_char(*c)?;
357                Ok(false)
358            }
359            Self::DoubleQuote => {
360                w.write_char('"')?;
361                Ok(true)
362            }
363            Self::SingleQuote => {
364                w.write_char('\'')?;
365                Ok(true)
366            }
367            Self::Backslash => {
368                w.write_char('\\')?;
369                Ok(true)
370            }
371            Self::Question => {
372                w.write_char('?')?;
373                Ok(true)
374            }
375            Self::Alert => {
376                w.write_char('\x07')?;
377                Ok(true)
378            }
379            Self::Backspace => {
380                w.write_char('\x08')?;
381                Ok(true)
382            }
383            Self::Escape => {
384                w.write_char('\x1B')?;
385                Ok(true)
386            }
387            Self::FormFeed => {
388                w.write_char('\x0C')?;
389                Ok(true)
390            }
391            Self::Newline => {
392                w.write_char('\n')?;
393                Ok(true)
394            }
395            Self::CarriageReturn => {
396                w.write_char('\r')?;
397                Ok(true)
398            }
399            Self::Tab => {
400                w.write_char('\t')?;
401                Ok(true)
402            }
403            Self::VerticalTab => {
404                w.write_char('\x0B')?;
405                Ok(true)
406            }
407            Self::Control(c) | Self::Octal(c) | Self::Hex(c) => {
408                // TODO: `c` should be treated as a raw byte rather than a
409                // Unicode scalar value. However, std::fmt::Write only supports
410                // UTF-8 strings.
411                w.write_char(*c as char)?;
412                Ok(true)
413            }
414            Self::Unicode(c) => {
415                w.write_char(*c)?;
416                Ok(true)
417            }
418        }
419    }
420}
421
422impl MaybeLiteral for EscapeUnit {
423    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
424        if let Self::Literal(c) = self {
425            result.extend(std::iter::once(*c));
426            Ok(())
427        } else {
428            Err(NotLiteral)
429        }
430    }
431}
432
433/// Converts an escaped string into the string represented by the escape
434/// sequences.
435///
436/// [Escape units](EscapeUnit) that do not represent valid Unicode scalar values
437/// are ignored.
438impl Unquote for EscapedString {
439    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
440        self.0.write_unquoted(w)
441    }
442}
443
444impl MaybeLiteral for EscapedString {
445    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
446        self.0.extend_literal(result)
447    }
448}
449
450impl Unquote for WordUnit {
451    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
452        match self {
453            Unquoted(inner) => inner.write_unquoted(w),
454            SingleQuote(inner) => {
455                w.write_str(inner)?;
456                Ok(true)
457            }
458            DoubleQuote(inner) => {
459                inner.write_unquoted(w)?;
460                Ok(true)
461            }
462            DollarSingleQuote(inner) => {
463                inner.write_unquoted(w)?;
464                Ok(true)
465            }
466            Tilde { name, .. } => {
467                write!(w, "~{name}")?;
468                Ok(false)
469            }
470        }
471    }
472}
473
474impl MaybeLiteral for WordUnit {
475    /// If `self` is `Unquoted(Literal(_))`, appends the character to `result`.
476    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
477        if let Unquoted(inner) = self {
478            inner.extend_literal(result)
479        } else {
480            Err(NotLiteral)
481        }
482    }
483}
484
485impl Unquote for Word {
486    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
487        self.units.write_unquoted(w)
488    }
489}
490
491impl MaybeLiteral for Word {
492    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
493        self.units.extend_literal(result)
494    }
495}
496
497/// Fallible conversion from a word into an assignment
498impl TryFrom<Word> for Assign {
499    type Error = Word;
500    /// Converts a word into an assignment.
501    ///
502    /// For a successful conversion, the word must be of the form `name=value`,
503    /// where `name` is a non-empty [literal](Word::to_string_if_literal) word,
504    /// `=` is an unquoted equal sign, and `value` is a word. If the input word
505    /// does not match this syntax, it is returned intact in `Err`.
506    fn try_from(mut word: Word) -> Result<Assign, Word> {
507        if let Some(eq) = word.units.iter().position(|u| u == &Unquoted(Literal('=')))
508            && eq > 0
509            && let Some(name) = word.units[..eq].to_string_if_literal()
510        {
511            assert!(!name.is_empty());
512            word.units.drain(..=eq);
513            word.parse_tilde_everywhere();
514            let location = word.location.clone();
515            let value = Scalar(word);
516            return Ok(Assign {
517                name,
518                value,
519                location,
520            });
521        }
522
523        Err(word)
524    }
525}
526
527impl TryFrom<Operator> for RedirOp {
528    type Error = TryFromOperatorError;
529    fn try_from(op: Operator) -> Result<RedirOp, TryFromOperatorError> {
530        use Operator::*;
531        use RedirOp::*;
532        match op {
533            Less => Ok(FileIn),
534            LessGreater => Ok(FileInOut),
535            Greater => Ok(FileOut),
536            GreaterGreater => Ok(FileAppend),
537            GreaterBar => Ok(FileClobber),
538            LessAnd => Ok(FdIn),
539            GreaterAnd => Ok(FdOut),
540            GreaterGreaterBar => Ok(Pipe),
541            LessLessLess => Ok(String),
542            _ => Err(TryFromOperatorError {}),
543        }
544    }
545}
546
547impl From<RedirOp> for Operator {
548    fn from(op: RedirOp) -> Operator {
549        use Operator::*;
550        use RedirOp::*;
551        match op {
552            FileIn => Less,
553            FileInOut => LessGreater,
554            FileOut => Greater,
555            FileAppend => GreaterGreater,
556            FileClobber => GreaterBar,
557            FdIn => LessAnd,
558            FdOut => GreaterAnd,
559            Pipe => GreaterGreaterBar,
560            String => LessLessLess,
561        }
562    }
563}
564
565impl<T: Into<Rc<HereDoc>>> From<T> for RedirBody {
566    fn from(t: T) -> Self {
567        RedirBody::HereDoc(t.into())
568    }
569}
570
571impl TryFrom<Operator> for CaseContinuation {
572    type Error = TryFromOperatorError;
573
574    /// Converts an operator into a case continuation.
575    ///
576    /// The `SemicolonBar` and `SemicolonSemicolonAnd` operators are converted
577    /// into `Continue`; you cannot distinguish between the two from the return
578    /// value.
579    fn try_from(op: Operator) -> Result<CaseContinuation, TryFromOperatorError> {
580        use CaseContinuation::*;
581        use Operator::*;
582        match op {
583            SemicolonSemicolon => Ok(Break),
584            SemicolonAnd => Ok(FallThrough),
585            SemicolonBar | SemicolonSemicolonAnd => Ok(Continue),
586            _ => Err(TryFromOperatorError {}),
587        }
588    }
589}
590
591impl From<CaseContinuation> for Operator {
592    /// Converts a case continuation into an operator.
593    ///
594    /// The `Continue` variant is converted into `SemicolonBar`.
595    fn from(cc: CaseContinuation) -> Operator {
596        use CaseContinuation::*;
597        use Operator::*;
598        match cc {
599            Break => SemicolonSemicolon,
600            FallThrough => SemicolonAnd,
601            Continue => SemicolonBar,
602        }
603    }
604}
605
606impl TryFrom<Operator> for AndOr {
607    type Error = TryFromOperatorError;
608    fn try_from(op: Operator) -> Result<AndOr, TryFromOperatorError> {
609        match op {
610            Operator::AndAnd => Ok(AndOr::AndThen),
611            Operator::BarBar => Ok(AndOr::OrElse),
612            _ => Err(TryFromOperatorError {}),
613        }
614    }
615}
616
617impl From<AndOr> for Operator {
618    fn from(op: AndOr) -> Operator {
619        match op {
620            AndOr::AndThen => Operator::AndAnd,
621            AndOr::OrElse => Operator::BarBar,
622        }
623    }
624}
625
626#[allow(
627    clippy::bool_assert_comparison,
628    reason = "to make the expected values clearer"
629)]
630#[cfg(test)]
631mod tests {
632    use super::*;
633    use assert_matches::assert_matches;
634
635    #[test]
636    fn special_param_from_str() {
637        assert_eq!("@".parse(), Ok(SpecialParam::At));
638        assert_eq!("*".parse(), Ok(SpecialParam::Asterisk));
639        assert_eq!("#".parse(), Ok(SpecialParam::Number));
640        assert_eq!("?".parse(), Ok(SpecialParam::Question));
641        assert_eq!("-".parse(), Ok(SpecialParam::Hyphen));
642        assert_eq!("$".parse(), Ok(SpecialParam::Dollar));
643        assert_eq!("!".parse(), Ok(SpecialParam::Exclamation));
644        assert_eq!("0".parse(), Ok(SpecialParam::Zero));
645
646        assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam));
647        assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam));
648        assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam));
649        assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam));
650    }
651
652    #[test]
653    fn switch_unquote() {
654        let switch = Switch {
655            action: SwitchAction::Default,
656            condition: SwitchCondition::UnsetOrEmpty,
657            word: "foo bar".parse().unwrap(),
658        };
659        let (unquoted, is_quoted) = switch.unquote();
660        assert_eq!(unquoted, ":-foo bar");
661        assert_eq!(is_quoted, false);
662
663        let switch = Switch {
664            action: SwitchAction::Error,
665            condition: SwitchCondition::Unset,
666            word: r"e\r\ror".parse().unwrap(),
667        };
668        let (unquoted, is_quoted) = switch.unquote();
669        assert_eq!(unquoted, "?error");
670        assert_eq!(is_quoted, true);
671    }
672
673    #[test]
674    fn trim_unquote() {
675        let trim = Trim {
676            side: TrimSide::Prefix,
677            length: TrimLength::Shortest,
678            pattern: "".parse().unwrap(),
679        };
680        let (unquoted, is_quoted) = trim.unquote();
681        assert_eq!(unquoted, "#");
682        assert_eq!(is_quoted, false);
683
684        let trim = Trim {
685            side: TrimSide::Prefix,
686            length: TrimLength::Longest,
687            pattern: "'yes'".parse().unwrap(),
688        };
689        let (unquoted, is_quoted) = trim.unquote();
690        assert_eq!(unquoted, "##yes");
691        assert_eq!(is_quoted, true);
692
693        let trim = Trim {
694            side: TrimSide::Suffix,
695            length: TrimLength::Shortest,
696            pattern: r"\no".parse().unwrap(),
697        };
698        let (unquoted, is_quoted) = trim.unquote();
699        assert_eq!(unquoted, "%no");
700        assert_eq!(is_quoted, true);
701
702        let trim = Trim {
703            side: TrimSide::Suffix,
704            length: TrimLength::Longest,
705            pattern: "?".parse().unwrap(),
706        };
707        let (unquoted, is_quoted) = trim.unquote();
708        assert_eq!(unquoted, "%%?");
709        assert_eq!(is_quoted, false);
710    }
711
712    #[test]
713    fn braced_param_unquote() {
714        let param = BracedParam {
715            param: Param::variable("foo"),
716            modifier: Modifier::None,
717            location: Location::dummy(""),
718        };
719        let (unquoted, is_quoted) = param.unquote();
720        assert_eq!(unquoted, "${foo}");
721        assert_eq!(is_quoted, false);
722
723        let param = BracedParam {
724            modifier: Modifier::Length,
725            ..param
726        };
727        let (unquoted, is_quoted) = param.unquote();
728        assert_eq!(unquoted, "${#foo}");
729        assert_eq!(is_quoted, false);
730
731        let switch = Switch {
732            action: SwitchAction::Assign,
733            condition: SwitchCondition::UnsetOrEmpty,
734            word: "'bar'".parse().unwrap(),
735        };
736        let param = BracedParam {
737            modifier: Modifier::Switch(switch),
738            ..param
739        };
740        let (unquoted, is_quoted) = param.unquote();
741        assert_eq!(unquoted, "${foo:=bar}");
742        assert_eq!(is_quoted, true);
743
744        let trim = Trim {
745            side: TrimSide::Suffix,
746            length: TrimLength::Shortest,
747            pattern: "baz' 'bar".parse().unwrap(),
748        };
749        let param = BracedParam {
750            modifier: Modifier::Trim(trim),
751            ..param
752        };
753        let (unquoted, is_quoted) = param.unquote();
754        assert_eq!(unquoted, "${foo%baz bar}");
755        assert_eq!(is_quoted, true);
756    }
757
758    #[test]
759    fn backquote_unit_unquote() {
760        let literal = BackquoteUnit::Literal('A');
761        let (unquoted, is_quoted) = literal.unquote();
762        assert_eq!(unquoted, "A");
763        assert_eq!(is_quoted, false);
764
765        let backslashed = BackquoteUnit::Backslashed('X');
766        let (unquoted, is_quoted) = backslashed.unquote();
767        assert_eq!(unquoted, "X");
768        assert_eq!(is_quoted, true);
769    }
770
771    #[test]
772    fn text_from_literal_chars() {
773        let text = Text::from_literal_chars(['a', '1'].iter().copied());
774        assert_eq!(text.0, [Literal('a'), Literal('1')]);
775    }
776
777    #[test]
778    fn text_unquote_without_quotes() {
779        let empty = Text(vec![]);
780        let (unquoted, is_quoted) = empty.unquote();
781        assert_eq!(unquoted, "");
782        assert_eq!(is_quoted, false);
783
784        let nonempty = Text(vec![
785            Literal('W'),
786            RawParam {
787                param: Param::variable("X"),
788                location: Location::dummy(""),
789            },
790            CommandSubst {
791                content: "Y".into(),
792                location: Location::dummy(""),
793            },
794            Backquote {
795                content: vec![BackquoteUnit::Literal('Z')],
796                location: Location::dummy(""),
797            },
798            Arith {
799                content: Text(vec![Literal('0')]),
800                location: Location::dummy(""),
801            },
802        ]);
803        let (unquoted, is_quoted) = nonempty.unquote();
804        assert_eq!(unquoted, "W$X$(Y)`Z`$((0))");
805        assert_eq!(is_quoted, false);
806    }
807
808    #[test]
809    fn text_unquote_with_quotes() {
810        let quoted = Text(vec![
811            Literal('a'),
812            Backslashed('b'),
813            Literal('c'),
814            Arith {
815                content: Text(vec![Literal('d')]),
816                location: Location::dummy(""),
817            },
818            Literal('e'),
819        ]);
820        let (unquoted, is_quoted) = quoted.unquote();
821        assert_eq!(unquoted, "abc$((d))e");
822        assert_eq!(is_quoted, true);
823
824        let content = vec![BackquoteUnit::Backslashed('X')];
825        let location = Location::dummy("");
826        let quoted = Text(vec![Backquote { content, location }]);
827        let (unquoted, is_quoted) = quoted.unquote();
828        assert_eq!(unquoted, "`X`");
829        assert_eq!(is_quoted, true);
830
831        let content = Text(vec![Backslashed('X')]);
832        let location = Location::dummy("");
833        let quoted = Text(vec![Arith { content, location }]);
834        let (unquoted, is_quoted) = quoted.unquote();
835        assert_eq!(unquoted, "$((X))");
836        assert_eq!(is_quoted, true);
837    }
838
839    #[test]
840    fn text_to_string_if_literal_success() {
841        let empty = Text(vec![]);
842        let s = empty.to_string_if_literal().unwrap();
843        assert_eq!(s, "");
844
845        let nonempty = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
846        let s = nonempty.to_string_if_literal().unwrap();
847        assert_eq!(s, "foo");
848    }
849
850    #[test]
851    fn text_to_string_if_literal_failure() {
852        let backslashed = Text(vec![Backslashed('a')]);
853        assert_eq!(backslashed.to_string_if_literal(), None);
854    }
855
856    #[test]
857    fn escape_unit_unquote() {
858        assert_eq!(EscapeUnit::Literal('A').unquote(), ("A".to_string(), false));
859        assert_eq!(EscapeUnit::DoubleQuote.unquote(), ("\"".to_string(), true));
860        assert_eq!(EscapeUnit::SingleQuote.unquote(), ("'".to_string(), true));
861        assert_eq!(EscapeUnit::Backslash.unquote(), ("\\".to_string(), true));
862        assert_eq!(EscapeUnit::Question.unquote(), ("?".to_string(), true));
863        assert_eq!(EscapeUnit::Alert.unquote(), ("\x07".to_string(), true));
864        assert_eq!(EscapeUnit::Backspace.unquote(), ("\x08".to_string(), true));
865        assert_eq!(EscapeUnit::Escape.unquote(), ("\x1B".to_string(), true));
866        assert_eq!(EscapeUnit::FormFeed.unquote(), ("\x0C".to_string(), true));
867        assert_eq!(EscapeUnit::Newline.unquote(), ("\n".to_string(), true));
868        assert_eq!(
869            EscapeUnit::CarriageReturn.unquote(),
870            ("\r".to_string(), true)
871        );
872        assert_eq!(EscapeUnit::Tab.unquote(), ("\t".to_string(), true));
873        assert_eq!(
874            EscapeUnit::VerticalTab.unquote(),
875            ("\x0B".to_string(), true)
876        );
877        assert_eq!(
878            EscapeUnit::Control(0x01).unquote(),
879            ("\x01".to_string(), true)
880        );
881        assert_eq!(
882            EscapeUnit::Control(0x1E).unquote(),
883            ("\x1E".to_string(), true)
884        );
885        assert_eq!(
886            EscapeUnit::Control(0x7F).unquote(),
887            ("\x7F".to_string(), true)
888        );
889        assert_eq!(EscapeUnit::Octal(0o123).unquote(), ("S".to_string(), true));
890        assert_eq!(EscapeUnit::Hex(0x41).unquote(), ("A".to_string(), true));
891        assert_eq!(
892            EscapeUnit::Unicode('🦀').unquote(),
893            ("🦀".to_string(), true)
894        );
895    }
896
897    #[test]
898    fn word_unquote() {
899        // A word's unquoted result is the concatenation of its units', and it
900        // is quoted if any of its units is quoted.
901        let word = Word::from_str(r#"a\b'c'"#).unwrap();
902        let (unquoted, is_quoted) = word.unquote();
903        assert_eq!(unquoted, "abc");
904        assert_eq!(is_quoted, true);
905
906        let word = Word::from_str("abc").unwrap();
907        let (unquoted, is_quoted) = word.unquote();
908        assert_eq!(unquoted, "abc");
909        assert_eq!(is_quoted, false);
910    }
911
912    #[test]
913    fn word_unit_unquote_unquoted() {
914        // The result of an unquoted word unit is that of its content.
915        let unit = Unquoted(Literal('a'));
916        let (unquoted, is_quoted) = unit.unquote();
917        assert_eq!(unquoted, "a");
918        assert_eq!(is_quoted, false);
919
920        let unit = Unquoted(Backslashed('a'));
921        let (unquoted, is_quoted) = unit.unquote();
922        assert_eq!(unquoted, "a");
923        assert_eq!(is_quoted, true);
924    }
925
926    #[test]
927    fn word_unit_unquote_single_quote() {
928        // A single-quoted word unit is always quoted, even if empty.
929        let unit = SingleQuote(String::new());
930        let (unquoted, is_quoted) = unit.unquote();
931        assert_eq!(unquoted, "");
932        assert_eq!(is_quoted, true);
933
934        let unit = SingleQuote(r#"a"b\c"#.to_string());
935        let (unquoted, is_quoted) = unit.unquote();
936        assert_eq!(unquoted, r#"a"b\c"#);
937        assert_eq!(is_quoted, true);
938    }
939
940    #[test]
941    fn word_unit_unquote_double_quote() {
942        // A double-quoted word unit is always quoted, even if its content
943        // consists only of literal characters.
944        let unit = DoubleQuote(Text(vec![Literal('E'), Literal('O'), Literal('F')]));
945        let (unquoted, is_quoted) = unit.unquote();
946        assert_eq!(unquoted, "EOF");
947        assert_eq!(is_quoted, true);
948
949        // The content of a double-quoted word unit is unquoted, so an
950        // expansion inside it is preserved.
951        let unit = DoubleQuote(Text(vec![RawParam {
952            param: Param::variable("foo"),
953            location: Location::dummy(""),
954        }]));
955        let (unquoted, is_quoted) = unit.unquote();
956        assert_eq!(unquoted, "$foo");
957        assert_eq!(is_quoted, true);
958    }
959
960    #[test]
961    fn word_unit_unquote_dollar_single_quote() {
962        // A dollar-single-quoted word unit is always quoted, even if its
963        // content consists only of literal characters.
964        let unit = DollarSingleQuote(EscapedString(vec![
965            EscapeUnit::Literal('E'),
966            EscapeUnit::Literal('O'),
967            EscapeUnit::Literal('F'),
968        ]));
969        let (unquoted, is_quoted) = unit.unquote();
970        assert_eq!(unquoted, "EOF");
971        assert_eq!(is_quoted, true);
972
973        let unit = DollarSingleQuote(EscapedString(vec![
974            EscapeUnit::Literal('a'),
975            EscapeUnit::Backslash,
976        ]));
977        let (unquoted, is_quoted) = unit.unquote();
978        assert_eq!(unquoted, r"a\");
979        assert_eq!(is_quoted, true);
980    }
981
982    #[test]
983    fn word_unit_unquote_tilde() {
984        // A tilde word unit is not quoted.
985        let unit = Tilde {
986            name: String::new(),
987            followed_by_slash: false,
988        };
989        let (unquoted, is_quoted) = unit.unquote();
990        assert_eq!(unquoted, "~");
991        assert_eq!(is_quoted, false);
992
993        let unit = Tilde {
994            name: "foo".to_string(),
995            followed_by_slash: true,
996        };
997        let (unquoted, is_quoted) = unit.unquote();
998        assert_eq!(unquoted, "~foo");
999        assert_eq!(is_quoted, false);
1000    }
1001
1002    #[test]
1003    fn word_to_string_if_literal_success() {
1004        let empty = Word::from_str("").unwrap();
1005        let s = empty.to_string_if_literal().unwrap();
1006        assert_eq!(s, "");
1007
1008        let nonempty = Word::from_str("~foo").unwrap();
1009        let s = nonempty.to_string_if_literal().unwrap();
1010        assert_eq!(s, "~foo");
1011    }
1012
1013    #[test]
1014    fn word_to_string_if_literal_failure() {
1015        let location = Location::dummy("foo");
1016        let backslashed = Unquoted(Backslashed('?'));
1017        let word = Word {
1018            units: vec![backslashed],
1019            location,
1020        };
1021        assert_eq!(word.to_string_if_literal(), None);
1022
1023        let word = Word {
1024            units: vec![Tilde {
1025                name: "foo".to_string(),
1026                followed_by_slash: false,
1027            }],
1028            ..word
1029        };
1030        assert_eq!(word.to_string_if_literal(), None);
1031    }
1032
1033    #[test]
1034    fn assign_try_from_word_without_equal() {
1035        let word = Word::from_str("foo").unwrap();
1036        let result = Assign::try_from(word.clone());
1037        assert_eq!(result.unwrap_err(), word);
1038    }
1039
1040    #[test]
1041    fn assign_try_from_word_with_empty_name() {
1042        let word = Word::from_str("=foo").unwrap();
1043        let result = Assign::try_from(word.clone());
1044        assert_eq!(result.unwrap_err(), word);
1045    }
1046
1047    #[test]
1048    fn assign_try_from_word_with_non_literal_name() {
1049        let mut word = Word::from_str("night=foo").unwrap();
1050        word.units.insert(0, Unquoted(Backslashed('k')));
1051        let result = Assign::try_from(word.clone());
1052        assert_eq!(result.unwrap_err(), word);
1053    }
1054
1055    #[test]
1056    fn assign_try_from_word_with_literal_name() {
1057        let word = Word::from_str("night=foo").unwrap();
1058        let location = word.location.clone();
1059        let assign = Assign::try_from(word).unwrap();
1060        assert_eq!(assign.name, "night");
1061        assert_matches!(assign.value, Scalar(value) => {
1062            assert_eq!(value.to_string(), "foo");
1063            assert_eq!(value.location, location);
1064        });
1065        assert_eq!(assign.location, location);
1066    }
1067
1068    #[test]
1069    fn assign_try_from_word_tilde() {
1070        let word = Word::from_str("a=~:~b").unwrap();
1071        let assign = Assign::try_from(word).unwrap();
1072        assert_matches!(assign.value, Scalar(value) => {
1073            assert_eq!(
1074                value.units,
1075                [
1076                    WordUnit::Tilde{
1077                        name: "".to_string(),
1078                        followed_by_slash: false,
1079                    },
1080                    WordUnit::Unquoted(TextUnit::Literal(':')),
1081                    WordUnit::Tilde {
1082                        name: "b".to_string(),
1083                        followed_by_slash: false,
1084                    },
1085                ]
1086            );
1087        });
1088    }
1089
1090    #[test]
1091    fn redir_op_conversions() {
1092        use RedirOp::*;
1093        for op in &[
1094            FileIn,
1095            FileInOut,
1096            FileOut,
1097            FileAppend,
1098            FileClobber,
1099            FdIn,
1100            FdOut,
1101            Pipe,
1102            String,
1103        ] {
1104            let op2 = RedirOp::try_from(Operator::from(*op));
1105            assert_eq!(op2, Ok(*op));
1106        }
1107    }
1108
1109    #[test]
1110    fn case_continuation_conversions() {
1111        use CaseContinuation::*;
1112        for cc in &[Break, FallThrough, Continue] {
1113            let cc2 = CaseContinuation::try_from(Operator::from(*cc));
1114            assert_eq!(cc2, Ok(*cc));
1115        }
1116        assert_eq!(
1117            CaseContinuation::try_from(Operator::SemicolonSemicolonAnd),
1118            Ok(Continue)
1119        );
1120    }
1121
1122    #[test]
1123    fn and_or_conversions() {
1124        for op in &[AndOr::AndThen, AndOr::OrElse] {
1125            let op2 = AndOr::try_from(Operator::from(*op));
1126            assert_eq!(op2, Ok(*op));
1127        }
1128    }
1129}