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 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 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 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}