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