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