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