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