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) => 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(
622    clippy::bool_assert_comparison,
623    reason = "to make the expected values clearer"
624)]
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use assert_matches::assert_matches;
629
630    #[test]
631    fn special_param_from_str() {
632        assert_eq!("@".parse(), Ok(SpecialParam::At));
633        assert_eq!("*".parse(), Ok(SpecialParam::Asterisk));
634        assert_eq!("#".parse(), Ok(SpecialParam::Number));
635        assert_eq!("?".parse(), Ok(SpecialParam::Question));
636        assert_eq!("-".parse(), Ok(SpecialParam::Hyphen));
637        assert_eq!("$".parse(), Ok(SpecialParam::Dollar));
638        assert_eq!("!".parse(), Ok(SpecialParam::Exclamation));
639        assert_eq!("0".parse(), Ok(SpecialParam::Zero));
640
641        assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam));
642        assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam));
643        assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam));
644        assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam));
645    }
646
647    #[test]
648    fn switch_unquote() {
649        let switch = Switch {
650            action: SwitchAction::Default,
651            condition: SwitchCondition::UnsetOrEmpty,
652            word: "foo bar".parse().unwrap(),
653        };
654        let (unquoted, is_quoted) = switch.unquote();
655        assert_eq!(unquoted, ":-foo bar");
656        assert_eq!(is_quoted, false);
657
658        let switch = Switch {
659            action: SwitchAction::Error,
660            condition: SwitchCondition::Unset,
661            word: r"e\r\ror".parse().unwrap(),
662        };
663        let (unquoted, is_quoted) = switch.unquote();
664        assert_eq!(unquoted, "?error");
665        assert_eq!(is_quoted, true);
666    }
667
668    #[test]
669    fn trim_unquote() {
670        let trim = Trim {
671            side: TrimSide::Prefix,
672            length: TrimLength::Shortest,
673            pattern: "".parse().unwrap(),
674        };
675        let (unquoted, is_quoted) = trim.unquote();
676        assert_eq!(unquoted, "#");
677        assert_eq!(is_quoted, false);
678
679        let trim = Trim {
680            side: TrimSide::Prefix,
681            length: TrimLength::Longest,
682            pattern: "'yes'".parse().unwrap(),
683        };
684        let (unquoted, is_quoted) = trim.unquote();
685        assert_eq!(unquoted, "##yes");
686        assert_eq!(is_quoted, true);
687
688        let trim = Trim {
689            side: TrimSide::Suffix,
690            length: TrimLength::Shortest,
691            pattern: r"\no".parse().unwrap(),
692        };
693        let (unquoted, is_quoted) = trim.unquote();
694        assert_eq!(unquoted, "%no");
695        assert_eq!(is_quoted, true);
696
697        let trim = Trim {
698            side: TrimSide::Suffix,
699            length: TrimLength::Longest,
700            pattern: "?".parse().unwrap(),
701        };
702        let (unquoted, is_quoted) = trim.unquote();
703        assert_eq!(unquoted, "%%?");
704        assert_eq!(is_quoted, false);
705    }
706
707    #[test]
708    fn braced_param_unquote() {
709        let param = BracedParam {
710            param: Param::variable("foo"),
711            modifier: Modifier::None,
712            location: Location::dummy(""),
713        };
714        let (unquoted, is_quoted) = param.unquote();
715        assert_eq!(unquoted, "${foo}");
716        assert_eq!(is_quoted, false);
717
718        let param = BracedParam {
719            modifier: Modifier::Length,
720            ..param
721        };
722        let (unquoted, is_quoted) = param.unquote();
723        assert_eq!(unquoted, "${#foo}");
724        assert_eq!(is_quoted, false);
725
726        let switch = Switch {
727            action: SwitchAction::Assign,
728            condition: SwitchCondition::UnsetOrEmpty,
729            word: "'bar'".parse().unwrap(),
730        };
731        let param = BracedParam {
732            modifier: Modifier::Switch(switch),
733            ..param
734        };
735        let (unquoted, is_quoted) = param.unquote();
736        assert_eq!(unquoted, "${foo:=bar}");
737        assert_eq!(is_quoted, true);
738
739        let trim = Trim {
740            side: TrimSide::Suffix,
741            length: TrimLength::Shortest,
742            pattern: "baz' 'bar".parse().unwrap(),
743        };
744        let param = BracedParam {
745            modifier: Modifier::Trim(trim),
746            ..param
747        };
748        let (unquoted, is_quoted) = param.unquote();
749        assert_eq!(unquoted, "${foo%baz bar}");
750        assert_eq!(is_quoted, true);
751    }
752
753    #[test]
754    fn backquote_unit_unquote() {
755        let literal = BackquoteUnit::Literal('A');
756        let (unquoted, is_quoted) = literal.unquote();
757        assert_eq!(unquoted, "A");
758        assert_eq!(is_quoted, false);
759
760        let backslashed = BackquoteUnit::Backslashed('X');
761        let (unquoted, is_quoted) = backslashed.unquote();
762        assert_eq!(unquoted, "X");
763        assert_eq!(is_quoted, true);
764    }
765
766    #[test]
767    fn text_from_literal_chars() {
768        let text = Text::from_literal_chars(['a', '1'].iter().copied());
769        assert_eq!(text.0, [Literal('a'), Literal('1')]);
770    }
771
772    #[test]
773    fn text_unquote_without_quotes() {
774        let empty = Text(vec![]);
775        let (unquoted, is_quoted) = empty.unquote();
776        assert_eq!(unquoted, "");
777        assert_eq!(is_quoted, false);
778
779        let nonempty = Text(vec![
780            Literal('W'),
781            RawParam {
782                param: Param::variable("X"),
783                location: Location::dummy(""),
784            },
785            CommandSubst {
786                content: "Y".into(),
787                location: Location::dummy(""),
788            },
789            Backquote {
790                content: vec![BackquoteUnit::Literal('Z')],
791                location: Location::dummy(""),
792            },
793            Arith {
794                content: Text(vec![Literal('0')]),
795                location: Location::dummy(""),
796            },
797        ]);
798        let (unquoted, is_quoted) = nonempty.unquote();
799        assert_eq!(unquoted, "W$X$(Y)`Z`$((0))");
800        assert_eq!(is_quoted, false);
801    }
802
803    #[test]
804    fn text_unquote_with_quotes() {
805        let quoted = Text(vec![
806            Literal('a'),
807            Backslashed('b'),
808            Literal('c'),
809            Arith {
810                content: Text(vec![Literal('d')]),
811                location: Location::dummy(""),
812            },
813            Literal('e'),
814        ]);
815        let (unquoted, is_quoted) = quoted.unquote();
816        assert_eq!(unquoted, "abc$((d))e");
817        assert_eq!(is_quoted, true);
818
819        let content = vec![BackquoteUnit::Backslashed('X')];
820        let location = Location::dummy("");
821        let quoted = Text(vec![Backquote { content, location }]);
822        let (unquoted, is_quoted) = quoted.unquote();
823        assert_eq!(unquoted, "`X`");
824        assert_eq!(is_quoted, true);
825
826        let content = Text(vec![Backslashed('X')]);
827        let location = Location::dummy("");
828        let quoted = Text(vec![Arith { content, location }]);
829        let (unquoted, is_quoted) = quoted.unquote();
830        assert_eq!(unquoted, "$((X))");
831        assert_eq!(is_quoted, true);
832    }
833
834    #[test]
835    fn text_to_string_if_literal_success() {
836        let empty = Text(vec![]);
837        let s = empty.to_string_if_literal().unwrap();
838        assert_eq!(s, "");
839
840        let nonempty = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
841        let s = nonempty.to_string_if_literal().unwrap();
842        assert_eq!(s, "foo");
843    }
844
845    #[test]
846    fn text_to_string_if_literal_failure() {
847        let backslashed = Text(vec![Backslashed('a')]);
848        assert_eq!(backslashed.to_string_if_literal(), None);
849    }
850
851    #[test]
852    fn escape_unit_unquote() {
853        assert_eq!(EscapeUnit::Literal('A').unquote(), ("A".to_string(), false));
854        assert_eq!(EscapeUnit::DoubleQuote.unquote(), ("\"".to_string(), true));
855        assert_eq!(EscapeUnit::SingleQuote.unquote(), ("'".to_string(), true));
856        assert_eq!(EscapeUnit::Backslash.unquote(), ("\\".to_string(), true));
857        assert_eq!(EscapeUnit::Question.unquote(), ("?".to_string(), true));
858        assert_eq!(EscapeUnit::Alert.unquote(), ("\x07".to_string(), true));
859        assert_eq!(EscapeUnit::Backspace.unquote(), ("\x08".to_string(), true));
860        assert_eq!(EscapeUnit::Escape.unquote(), ("\x1B".to_string(), true));
861        assert_eq!(EscapeUnit::FormFeed.unquote(), ("\x0C".to_string(), true));
862        assert_eq!(EscapeUnit::Newline.unquote(), ("\n".to_string(), true));
863        assert_eq!(
864            EscapeUnit::CarriageReturn.unquote(),
865            ("\r".to_string(), true)
866        );
867        assert_eq!(EscapeUnit::Tab.unquote(), ("\t".to_string(), true));
868        assert_eq!(
869            EscapeUnit::VerticalTab.unquote(),
870            ("\x0B".to_string(), true)
871        );
872        assert_eq!(
873            EscapeUnit::Control(0x01).unquote(),
874            ("\x01".to_string(), true)
875        );
876        assert_eq!(
877            EscapeUnit::Control(0x1E).unquote(),
878            ("\x1E".to_string(), true)
879        );
880        assert_eq!(
881            EscapeUnit::Control(0x7F).unquote(),
882            ("\x7F".to_string(), true)
883        );
884        assert_eq!(EscapeUnit::Octal(0o123).unquote(), ("S".to_string(), true));
885        assert_eq!(EscapeUnit::Hex(0x41).unquote(), ("A".to_string(), true));
886        assert_eq!(
887            EscapeUnit::Unicode('🦀').unquote(),
888            ("🦀".to_string(), true)
889        );
890    }
891
892    #[test]
893    fn word_unquote() {
894        let mut word = Word::from_str(r#"~a/b\c'd'"e""#).unwrap();
895        let (unquoted, is_quoted) = word.unquote();
896        assert_eq!(unquoted, "~a/bcde");
897        assert_eq!(is_quoted, true);
898
899        word.parse_tilde_front();
900        let (unquoted, is_quoted) = word.unquote();
901        assert_eq!(unquoted, "~a/bcde");
902        assert_eq!(is_quoted, true);
903    }
904
905    #[test]
906    fn word_to_string_if_literal_success() {
907        let empty = Word::from_str("").unwrap();
908        let s = empty.to_string_if_literal().unwrap();
909        assert_eq!(s, "");
910
911        let nonempty = Word::from_str("~foo").unwrap();
912        let s = nonempty.to_string_if_literal().unwrap();
913        assert_eq!(s, "~foo");
914    }
915
916    #[test]
917    fn word_to_string_if_literal_failure() {
918        let location = Location::dummy("foo");
919        let backslashed = Unquoted(Backslashed('?'));
920        let word = Word {
921            units: vec![backslashed],
922            location,
923        };
924        assert_eq!(word.to_string_if_literal(), None);
925
926        let word = Word {
927            units: vec![Tilde {
928                name: "foo".to_string(),
929                followed_by_slash: false,
930            }],
931            ..word
932        };
933        assert_eq!(word.to_string_if_literal(), None);
934    }
935
936    #[test]
937    fn assign_try_from_word_without_equal() {
938        let word = Word::from_str("foo").unwrap();
939        let result = Assign::try_from(word.clone());
940        assert_eq!(result.unwrap_err(), word);
941    }
942
943    #[test]
944    fn assign_try_from_word_with_empty_name() {
945        let word = Word::from_str("=foo").unwrap();
946        let result = Assign::try_from(word.clone());
947        assert_eq!(result.unwrap_err(), word);
948    }
949
950    #[test]
951    fn assign_try_from_word_with_non_literal_name() {
952        let mut word = Word::from_str("night=foo").unwrap();
953        word.units.insert(0, Unquoted(Backslashed('k')));
954        let result = Assign::try_from(word.clone());
955        assert_eq!(result.unwrap_err(), word);
956    }
957
958    #[test]
959    fn assign_try_from_word_with_literal_name() {
960        let word = Word::from_str("night=foo").unwrap();
961        let location = word.location.clone();
962        let assign = Assign::try_from(word).unwrap();
963        assert_eq!(assign.name, "night");
964        assert_matches!(assign.value, Scalar(value) => {
965            assert_eq!(value.to_string(), "foo");
966            assert_eq!(value.location, location);
967        });
968        assert_eq!(assign.location, location);
969    }
970
971    #[test]
972    fn assign_try_from_word_tilde() {
973        let word = Word::from_str("a=~:~b").unwrap();
974        let assign = Assign::try_from(word).unwrap();
975        assert_matches!(assign.value, Scalar(value) => {
976            assert_eq!(
977                value.units,
978                [
979                    WordUnit::Tilde{
980                        name: "".to_string(),
981                        followed_by_slash: false,
982                    },
983                    WordUnit::Unquoted(TextUnit::Literal(':')),
984                    WordUnit::Tilde {
985                        name: "b".to_string(),
986                        followed_by_slash: false,
987                    },
988                ]
989            );
990        });
991    }
992
993    #[test]
994    fn redir_op_conversions() {
995        use RedirOp::*;
996        for op in &[
997            FileIn,
998            FileInOut,
999            FileOut,
1000            FileAppend,
1001            FileClobber,
1002            FdIn,
1003            FdOut,
1004            Pipe,
1005            String,
1006        ] {
1007            let op2 = RedirOp::try_from(Operator::from(*op));
1008            assert_eq!(op2, Ok(*op));
1009        }
1010    }
1011
1012    #[test]
1013    fn case_continuation_conversions() {
1014        use CaseContinuation::*;
1015        for cc in &[Break, FallThrough, Continue] {
1016            let cc2 = CaseContinuation::try_from(Operator::from(*cc));
1017            assert_eq!(cc2, Ok(*cc));
1018        }
1019        assert_eq!(
1020            CaseContinuation::try_from(Operator::SemicolonSemicolonAnd),
1021            Ok(Continue)
1022        );
1023    }
1024
1025    #[test]
1026    fn and_or_conversions() {
1027        for op in &[AndOr::AndThen, AndOr::OrElse] {
1028            let op2 = AndOr::try_from(Operator::from(*op));
1029            assert_eq!(op2, Ok(*op));
1030        }
1031    }
1032}