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");
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) => inner.write_unquoted(w),
459            DollarSingleQuote(inner) => inner.write_unquoted(w),
460            Tilde { name, .. } => {
461                write!(w, "~{name}")?;
462                Ok(false)
463            }
464        }
465    }
466}
467
468impl MaybeLiteral for WordUnit {
469    /// If `self` is `Unquoted(Literal(_))`, appends the character to `result`.
470    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
471        if let Unquoted(inner) = self {
472            inner.extend_literal(result)
473        } else {
474            Err(NotLiteral)
475        }
476    }
477}
478
479impl Unquote for Word {
480    fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
481        self.units.write_unquoted(w)
482    }
483}
484
485impl MaybeLiteral for Word {
486    fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
487        self.units.extend_literal(result)
488    }
489}
490
491/// Fallible conversion from a word into an assignment
492impl TryFrom<Word> for Assign {
493    type Error = Word;
494    /// Converts a word into an assignment.
495    ///
496    /// For a successful conversion, the word must be of the form `name=value`,
497    /// where `name` is a non-empty [literal](Word::to_string_if_literal) word,
498    /// `=` is an unquoted equal sign, and `value` is a word. If the input word
499    /// does not match this syntax, it is returned intact in `Err`.
500    fn try_from(mut word: Word) -> Result<Assign, Word> {
501        if let Some(eq) = word.units.iter().position(|u| u == &Unquoted(Literal('='))) {
502            if eq > 0 {
503                if let Some(name) = word.units[..eq].to_string_if_literal() {
504                    assert!(!name.is_empty());
505                    word.units.drain(..=eq);
506                    word.parse_tilde_everywhere();
507                    let location = word.location.clone();
508                    let value = Scalar(word);
509                    return Ok(Assign {
510                        name,
511                        value,
512                        location,
513                    });
514                }
515            }
516        }
517
518        Err(word)
519    }
520}
521
522impl TryFrom<Operator> for RedirOp {
523    type Error = TryFromOperatorError;
524    fn try_from(op: Operator) -> Result<RedirOp, TryFromOperatorError> {
525        use Operator::*;
526        use RedirOp::*;
527        match op {
528            Less => Ok(FileIn),
529            LessGreater => Ok(FileInOut),
530            Greater => Ok(FileOut),
531            GreaterGreater => Ok(FileAppend),
532            GreaterBar => Ok(FileClobber),
533            LessAnd => Ok(FdIn),
534            GreaterAnd => Ok(FdOut),
535            GreaterGreaterBar => Ok(Pipe),
536            LessLessLess => Ok(String),
537            _ => Err(TryFromOperatorError {}),
538        }
539    }
540}
541
542impl From<RedirOp> for Operator {
543    fn from(op: RedirOp) -> Operator {
544        use Operator::*;
545        use RedirOp::*;
546        match op {
547            FileIn => Less,
548            FileInOut => LessGreater,
549            FileOut => Greater,
550            FileAppend => GreaterGreater,
551            FileClobber => GreaterBar,
552            FdIn => LessAnd,
553            FdOut => GreaterAnd,
554            Pipe => GreaterGreaterBar,
555            String => LessLessLess,
556        }
557    }
558}
559
560impl<T: Into<Rc<HereDoc>>> From<T> for RedirBody {
561    fn from(t: T) -> Self {
562        RedirBody::HereDoc(t.into())
563    }
564}
565
566impl TryFrom<Operator> for CaseContinuation {
567    type Error = TryFromOperatorError;
568
569    /// Converts an operator into a case continuation.
570    ///
571    /// The `SemicolonBar` and `SemicolonSemicolonAnd` operators are converted
572    /// into `Continue`; you cannot distinguish between the two from the return
573    /// value.
574    fn try_from(op: Operator) -> Result<CaseContinuation, TryFromOperatorError> {
575        use CaseContinuation::*;
576        use Operator::*;
577        match op {
578            SemicolonSemicolon => Ok(Break),
579            SemicolonAnd => Ok(FallThrough),
580            SemicolonBar | SemicolonSemicolonAnd => Ok(Continue),
581            _ => Err(TryFromOperatorError {}),
582        }
583    }
584}
585
586impl From<CaseContinuation> for Operator {
587    /// Converts a case continuation into an operator.
588    ///
589    /// The `Continue` variant is converted into `SemicolonBar`.
590    fn from(cc: CaseContinuation) -> Operator {
591        use CaseContinuation::*;
592        use Operator::*;
593        match cc {
594            Break => SemicolonSemicolon,
595            FallThrough => SemicolonAnd,
596            Continue => SemicolonBar,
597        }
598    }
599}
600
601impl TryFrom<Operator> for AndOr {
602    type Error = TryFromOperatorError;
603    fn try_from(op: Operator) -> Result<AndOr, TryFromOperatorError> {
604        match op {
605            Operator::AndAnd => Ok(AndOr::AndThen),
606            Operator::BarBar => Ok(AndOr::OrElse),
607            _ => Err(TryFromOperatorError {}),
608        }
609    }
610}
611
612impl From<AndOr> for Operator {
613    fn from(op: AndOr) -> Operator {
614        match op {
615            AndOr::AndThen => Operator::AndAnd,
616            AndOr::OrElse => Operator::BarBar,
617        }
618    }
619}
620
621#[allow(clippy::bool_assert_comparison)]
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use assert_matches::assert_matches;
626
627    #[test]
628    fn special_param_from_str() {
629        assert_eq!("@".parse(), Ok(SpecialParam::At));
630        assert_eq!("*".parse(), Ok(SpecialParam::Asterisk));
631        assert_eq!("#".parse(), Ok(SpecialParam::Number));
632        assert_eq!("?".parse(), Ok(SpecialParam::Question));
633        assert_eq!("-".parse(), Ok(SpecialParam::Hyphen));
634        assert_eq!("$".parse(), Ok(SpecialParam::Dollar));
635        assert_eq!("!".parse(), Ok(SpecialParam::Exclamation));
636        assert_eq!("0".parse(), Ok(SpecialParam::Zero));
637
638        assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam));
639        assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam));
640        assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam));
641        assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam));
642    }
643
644    #[test]
645    fn switch_unquote() {
646        let switch = Switch {
647            action: SwitchAction::Default,
648            condition: SwitchCondition::UnsetOrEmpty,
649            word: "foo bar".parse().unwrap(),
650        };
651        let (unquoted, is_quoted) = switch.unquote();
652        assert_eq!(unquoted, ":-foo bar");
653        assert_eq!(is_quoted, false);
654
655        let switch = Switch {
656            action: SwitchAction::Error,
657            condition: SwitchCondition::Unset,
658            word: r"e\r\ror".parse().unwrap(),
659        };
660        let (unquoted, is_quoted) = switch.unquote();
661        assert_eq!(unquoted, "?error");
662        assert_eq!(is_quoted, true);
663    }
664
665    #[test]
666    fn trim_unquote() {
667        let trim = Trim {
668            side: TrimSide::Prefix,
669            length: TrimLength::Shortest,
670            pattern: "".parse().unwrap(),
671        };
672        let (unquoted, is_quoted) = trim.unquote();
673        assert_eq!(unquoted, "#");
674        assert_eq!(is_quoted, false);
675
676        let trim = Trim {
677            side: TrimSide::Prefix,
678            length: TrimLength::Longest,
679            pattern: "'yes'".parse().unwrap(),
680        };
681        let (unquoted, is_quoted) = trim.unquote();
682        assert_eq!(unquoted, "##yes");
683        assert_eq!(is_quoted, true);
684
685        let trim = Trim {
686            side: TrimSide::Suffix,
687            length: TrimLength::Shortest,
688            pattern: r"\no".parse().unwrap(),
689        };
690        let (unquoted, is_quoted) = trim.unquote();
691        assert_eq!(unquoted, "%no");
692        assert_eq!(is_quoted, true);
693
694        let trim = Trim {
695            side: TrimSide::Suffix,
696            length: TrimLength::Longest,
697            pattern: "?".parse().unwrap(),
698        };
699        let (unquoted, is_quoted) = trim.unquote();
700        assert_eq!(unquoted, "%%?");
701        assert_eq!(is_quoted, false);
702    }
703
704    #[test]
705    fn braced_param_unquote() {
706        let param = BracedParam {
707            param: Param::variable("foo"),
708            modifier: Modifier::None,
709            location: Location::dummy(""),
710        };
711        let (unquoted, is_quoted) = param.unquote();
712        assert_eq!(unquoted, "${foo}");
713        assert_eq!(is_quoted, false);
714
715        let param = BracedParam {
716            modifier: Modifier::Length,
717            ..param
718        };
719        let (unquoted, is_quoted) = param.unquote();
720        assert_eq!(unquoted, "${#foo}");
721        assert_eq!(is_quoted, false);
722
723        let switch = Switch {
724            action: SwitchAction::Assign,
725            condition: SwitchCondition::UnsetOrEmpty,
726            word: "'bar'".parse().unwrap(),
727        };
728        let param = BracedParam {
729            modifier: Modifier::Switch(switch),
730            ..param
731        };
732        let (unquoted, is_quoted) = param.unquote();
733        assert_eq!(unquoted, "${foo:=bar}");
734        assert_eq!(is_quoted, true);
735
736        let trim = Trim {
737            side: TrimSide::Suffix,
738            length: TrimLength::Shortest,
739            pattern: "baz' 'bar".parse().unwrap(),
740        };
741        let param = BracedParam {
742            modifier: Modifier::Trim(trim),
743            ..param
744        };
745        let (unquoted, is_quoted) = param.unquote();
746        assert_eq!(unquoted, "${foo%baz bar}");
747        assert_eq!(is_quoted, true);
748    }
749
750    #[test]
751    fn backquote_unit_unquote() {
752        let literal = BackquoteUnit::Literal('A');
753        let (unquoted, is_quoted) = literal.unquote();
754        assert_eq!(unquoted, "A");
755        assert_eq!(is_quoted, false);
756
757        let backslashed = BackquoteUnit::Backslashed('X');
758        let (unquoted, is_quoted) = backslashed.unquote();
759        assert_eq!(unquoted, "X");
760        assert_eq!(is_quoted, true);
761    }
762
763    #[test]
764    fn text_from_literal_chars() {
765        let text = Text::from_literal_chars(['a', '1'].iter().copied());
766        assert_eq!(text.0, [Literal('a'), Literal('1')]);
767    }
768
769    #[test]
770    fn text_unquote_without_quotes() {
771        let empty = Text(vec![]);
772        let (unquoted, is_quoted) = empty.unquote();
773        assert_eq!(unquoted, "");
774        assert_eq!(is_quoted, false);
775
776        let nonempty = Text(vec![
777            Literal('W'),
778            RawParam {
779                param: Param::variable("X"),
780                location: Location::dummy(""),
781            },
782            CommandSubst {
783                content: "Y".into(),
784                location: Location::dummy(""),
785            },
786            Backquote {
787                content: vec![BackquoteUnit::Literal('Z')],
788                location: Location::dummy(""),
789            },
790            Arith {
791                content: Text(vec![Literal('0')]),
792                location: Location::dummy(""),
793            },
794        ]);
795        let (unquoted, is_quoted) = nonempty.unquote();
796        assert_eq!(unquoted, "W$X$(Y)`Z`$((0))");
797        assert_eq!(is_quoted, false);
798    }
799
800    #[test]
801    fn text_unquote_with_quotes() {
802        let quoted = Text(vec![
803            Literal('a'),
804            Backslashed('b'),
805            Literal('c'),
806            Arith {
807                content: Text(vec![Literal('d')]),
808                location: Location::dummy(""),
809            },
810            Literal('e'),
811        ]);
812        let (unquoted, is_quoted) = quoted.unquote();
813        assert_eq!(unquoted, "abc$((d))e");
814        assert_eq!(is_quoted, true);
815
816        let content = vec![BackquoteUnit::Backslashed('X')];
817        let location = Location::dummy("");
818        let quoted = Text(vec![Backquote { content, location }]);
819        let (unquoted, is_quoted) = quoted.unquote();
820        assert_eq!(unquoted, "`X`");
821        assert_eq!(is_quoted, true);
822
823        let content = Text(vec![Backslashed('X')]);
824        let location = Location::dummy("");
825        let quoted = Text(vec![Arith { content, location }]);
826        let (unquoted, is_quoted) = quoted.unquote();
827        assert_eq!(unquoted, "$((X))");
828        assert_eq!(is_quoted, true);
829    }
830
831    #[test]
832    fn text_to_string_if_literal_success() {
833        let empty = Text(vec![]);
834        let s = empty.to_string_if_literal().unwrap();
835        assert_eq!(s, "");
836
837        let nonempty = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
838        let s = nonempty.to_string_if_literal().unwrap();
839        assert_eq!(s, "foo");
840    }
841
842    #[test]
843    fn text_to_string_if_literal_failure() {
844        let backslashed = Text(vec![Backslashed('a')]);
845        assert_eq!(backslashed.to_string_if_literal(), None);
846    }
847
848    #[test]
849    fn escape_unit_unquote() {
850        assert_eq!(EscapeUnit::Literal('A').unquote(), ("A".to_string(), false));
851        assert_eq!(EscapeUnit::DoubleQuote.unquote(), ("\"".to_string(), true));
852        assert_eq!(EscapeUnit::SingleQuote.unquote(), ("'".to_string(), true));
853        assert_eq!(EscapeUnit::Backslash.unquote(), ("\\".to_string(), true));
854        assert_eq!(EscapeUnit::Question.unquote(), ("?".to_string(), true));
855        assert_eq!(EscapeUnit::Alert.unquote(), ("\x07".to_string(), true));
856        assert_eq!(EscapeUnit::Backspace.unquote(), ("\x08".to_string(), true));
857        assert_eq!(EscapeUnit::Escape.unquote(), ("\x1B".to_string(), true));
858        assert_eq!(EscapeUnit::FormFeed.unquote(), ("\x0C".to_string(), true));
859        assert_eq!(EscapeUnit::Newline.unquote(), ("\n".to_string(), true));
860        assert_eq!(
861            EscapeUnit::CarriageReturn.unquote(),
862            ("\r".to_string(), true)
863        );
864        assert_eq!(EscapeUnit::Tab.unquote(), ("\t".to_string(), true));
865        assert_eq!(
866            EscapeUnit::VerticalTab.unquote(),
867            ("\x0B".to_string(), true)
868        );
869        assert_eq!(
870            EscapeUnit::Control(0x01).unquote(),
871            ("\x01".to_string(), true)
872        );
873        assert_eq!(
874            EscapeUnit::Control(0x1E).unquote(),
875            ("\x1E".to_string(), true)
876        );
877        assert_eq!(
878            EscapeUnit::Control(0x7F).unquote(),
879            ("\x7F".to_string(), true)
880        );
881        assert_eq!(EscapeUnit::Octal(0o123).unquote(), ("S".to_string(), true));
882        assert_eq!(EscapeUnit::Hex(0x41).unquote(), ("A".to_string(), true));
883        assert_eq!(
884            EscapeUnit::Unicode('🦀').unquote(),
885            ("🦀".to_string(), true)
886        );
887    }
888
889    #[test]
890    fn word_unquote() {
891        let mut word = Word::from_str(r#"~a/b\c'd'"e""#).unwrap();
892        let (unquoted, is_quoted) = word.unquote();
893        assert_eq!(unquoted, "~a/bcde");
894        assert_eq!(is_quoted, true);
895
896        word.parse_tilde_front();
897        let (unquoted, is_quoted) = word.unquote();
898        assert_eq!(unquoted, "~a/bcde");
899        assert_eq!(is_quoted, true);
900    }
901
902    #[test]
903    fn word_to_string_if_literal_success() {
904        let empty = Word::from_str("").unwrap();
905        let s = empty.to_string_if_literal().unwrap();
906        assert_eq!(s, "");
907
908        let nonempty = Word::from_str("~foo").unwrap();
909        let s = nonempty.to_string_if_literal().unwrap();
910        assert_eq!(s, "~foo");
911    }
912
913    #[test]
914    fn word_to_string_if_literal_failure() {
915        let location = Location::dummy("foo");
916        let backslashed = Unquoted(Backslashed('?'));
917        let word = Word {
918            units: vec![backslashed],
919            location,
920        };
921        assert_eq!(word.to_string_if_literal(), None);
922
923        let word = Word {
924            units: vec![Tilde {
925                name: "foo".to_string(),
926                followed_by_slash: false,
927            }],
928            ..word
929        };
930        assert_eq!(word.to_string_if_literal(), None);
931    }
932
933    #[test]
934    fn assign_try_from_word_without_equal() {
935        let word = Word::from_str("foo").unwrap();
936        let result = Assign::try_from(word.clone());
937        assert_eq!(result.unwrap_err(), word);
938    }
939
940    #[test]
941    fn assign_try_from_word_with_empty_name() {
942        let word = Word::from_str("=foo").unwrap();
943        let result = Assign::try_from(word.clone());
944        assert_eq!(result.unwrap_err(), word);
945    }
946
947    #[test]
948    fn assign_try_from_word_with_non_literal_name() {
949        let mut word = Word::from_str("night=foo").unwrap();
950        word.units.insert(0, Unquoted(Backslashed('k')));
951        let result = Assign::try_from(word.clone());
952        assert_eq!(result.unwrap_err(), word);
953    }
954
955    #[test]
956    fn assign_try_from_word_with_literal_name() {
957        let word = Word::from_str("night=foo").unwrap();
958        let location = word.location.clone();
959        let assign = Assign::try_from(word).unwrap();
960        assert_eq!(assign.name, "night");
961        assert_matches!(assign.value, Scalar(value) => {
962            assert_eq!(value.to_string(), "foo");
963            assert_eq!(value.location, location);
964        });
965        assert_eq!(assign.location, location);
966    }
967
968    #[test]
969    fn assign_try_from_word_tilde() {
970        let word = Word::from_str("a=~:~b").unwrap();
971        let assign = Assign::try_from(word).unwrap();
972        assert_matches!(assign.value, Scalar(value) => {
973            assert_eq!(
974                value.units,
975                [
976                    WordUnit::Tilde{
977                        name: "".to_string(),
978                        followed_by_slash: false,
979                    },
980                    WordUnit::Unquoted(TextUnit::Literal(':')),
981                    WordUnit::Tilde {
982                        name: "b".to_string(),
983                        followed_by_slash: false,
984                    },
985                ]
986            );
987        });
988    }
989
990    #[test]
991    fn redir_op_conversions() {
992        use RedirOp::*;
993        for op in &[
994            FileIn,
995            FileInOut,
996            FileOut,
997            FileAppend,
998            FileClobber,
999            FdIn,
1000            FdOut,
1001            Pipe,
1002            String,
1003        ] {
1004            let op2 = RedirOp::try_from(Operator::from(*op));
1005            assert_eq!(op2, Ok(*op));
1006        }
1007    }
1008
1009    #[test]
1010    fn case_continuation_conversions() {
1011        use CaseContinuation::*;
1012        for cc in &[Break, FallThrough, Continue] {
1013            let cc2 = CaseContinuation::try_from(Operator::from(*cc));
1014            assert_eq!(cc2, Ok(*cc));
1015        }
1016        assert_eq!(
1017            CaseContinuation::try_from(Operator::SemicolonSemicolonAnd),
1018            Ok(Continue)
1019        );
1020    }
1021
1022    #[test]
1023    fn and_or_conversions() {
1024        for op in &[AndOr::AndThen, AndOr::OrElse] {
1025            let op2 = AndOr::try_from(Operator::from(*op));
1026            assert_eq!(op2, Ok(*op));
1027        }
1028    }
1029}