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) => {
459 inner.write_unquoted(w)?;
460 Ok(true)
461 }
462 DollarSingleQuote(inner) => {
463 inner.write_unquoted(w)?;
464 Ok(true)
465 }
466 Tilde { name, .. } => {
467 write!(w, "~{name}")?;
468 Ok(false)
469 }
470 }
471 }
472}
473
474impl MaybeLiteral for WordUnit {
475 fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
477 if let Unquoted(inner) = self {
478 inner.extend_literal(result)
479 } else {
480 Err(NotLiteral)
481 }
482 }
483}
484
485impl Unquote for Word {
486 fn write_unquoted<W: fmt::Write>(&self, w: &mut W) -> UnquoteResult {
487 self.units.write_unquoted(w)
488 }
489}
490
491impl MaybeLiteral for Word {
492 fn extend_literal<T: Extend<char>>(&self, result: &mut T) -> Result<(), NotLiteral> {
493 self.units.extend_literal(result)
494 }
495}
496
497impl TryFrom<Word> for Assign {
499 type Error = Word;
500 fn try_from(mut word: Word) -> Result<Assign, Word> {
507 if let Some(eq) = word.units.iter().position(|u| u == &Unquoted(Literal('=')))
508 && eq > 0
509 && let Some(name) = word.units[..eq].to_string_if_literal()
510 {
511 assert!(!name.is_empty());
512 word.units.drain(..=eq);
513 word.parse_tilde_everywhere();
514 let location = word.location.clone();
515 let value = Scalar(word);
516 return Ok(Assign {
517 name,
518 value,
519 location,
520 });
521 }
522
523 Err(word)
524 }
525}
526
527impl TryFrom<Operator> for RedirOp {
528 type Error = TryFromOperatorError;
529 fn try_from(op: Operator) -> Result<RedirOp, TryFromOperatorError> {
530 use Operator::*;
531 use RedirOp::*;
532 match op {
533 Less => Ok(FileIn),
534 LessGreater => Ok(FileInOut),
535 Greater => Ok(FileOut),
536 GreaterGreater => Ok(FileAppend),
537 GreaterBar => Ok(FileClobber),
538 LessAnd => Ok(FdIn),
539 GreaterAnd => Ok(FdOut),
540 GreaterGreaterBar => Ok(Pipe),
541 LessLessLess => Ok(String),
542 _ => Err(TryFromOperatorError {}),
543 }
544 }
545}
546
547impl From<RedirOp> for Operator {
548 fn from(op: RedirOp) -> Operator {
549 use Operator::*;
550 use RedirOp::*;
551 match op {
552 FileIn => Less,
553 FileInOut => LessGreater,
554 FileOut => Greater,
555 FileAppend => GreaterGreater,
556 FileClobber => GreaterBar,
557 FdIn => LessAnd,
558 FdOut => GreaterAnd,
559 Pipe => GreaterGreaterBar,
560 String => LessLessLess,
561 }
562 }
563}
564
565impl<T: Into<Rc<HereDoc>>> From<T> for RedirBody {
566 fn from(t: T) -> Self {
567 RedirBody::HereDoc(t.into())
568 }
569}
570
571impl TryFrom<Operator> for CaseContinuation {
572 type Error = TryFromOperatorError;
573
574 fn try_from(op: Operator) -> Result<CaseContinuation, TryFromOperatorError> {
580 use CaseContinuation::*;
581 use Operator::*;
582 match op {
583 SemicolonSemicolon => Ok(Break),
584 SemicolonAnd => Ok(FallThrough),
585 SemicolonBar | SemicolonSemicolonAnd => Ok(Continue),
586 _ => Err(TryFromOperatorError {}),
587 }
588 }
589}
590
591impl From<CaseContinuation> for Operator {
592 fn from(cc: CaseContinuation) -> Operator {
596 use CaseContinuation::*;
597 use Operator::*;
598 match cc {
599 Break => SemicolonSemicolon,
600 FallThrough => SemicolonAnd,
601 Continue => SemicolonBar,
602 }
603 }
604}
605
606impl TryFrom<Operator> for AndOr {
607 type Error = TryFromOperatorError;
608 fn try_from(op: Operator) -> Result<AndOr, TryFromOperatorError> {
609 match op {
610 Operator::AndAnd => Ok(AndOr::AndThen),
611 Operator::BarBar => Ok(AndOr::OrElse),
612 _ => Err(TryFromOperatorError {}),
613 }
614 }
615}
616
617impl From<AndOr> for Operator {
618 fn from(op: AndOr) -> Operator {
619 match op {
620 AndOr::AndThen => Operator::AndAnd,
621 AndOr::OrElse => Operator::BarBar,
622 }
623 }
624}
625
626#[allow(
627 clippy::bool_assert_comparison,
628 reason = "to make the expected values clearer"
629)]
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use assert_matches::assert_matches;
634
635 #[test]
636 fn special_param_from_str() {
637 assert_eq!("@".parse(), Ok(SpecialParam::At));
638 assert_eq!("*".parse(), Ok(SpecialParam::Asterisk));
639 assert_eq!("#".parse(), Ok(SpecialParam::Number));
640 assert_eq!("?".parse(), Ok(SpecialParam::Question));
641 assert_eq!("-".parse(), Ok(SpecialParam::Hyphen));
642 assert_eq!("$".parse(), Ok(SpecialParam::Dollar));
643 assert_eq!("!".parse(), Ok(SpecialParam::Exclamation));
644 assert_eq!("0".parse(), Ok(SpecialParam::Zero));
645
646 assert_eq!(SpecialParam::from_str(""), Err(NotSpecialParam));
647 assert_eq!(SpecialParam::from_str("##"), Err(NotSpecialParam));
648 assert_eq!(SpecialParam::from_str("1"), Err(NotSpecialParam));
649 assert_eq!(SpecialParam::from_str("00"), Err(NotSpecialParam));
650 }
651
652 #[test]
653 fn switch_unquote() {
654 let switch = Switch {
655 action: SwitchAction::Default,
656 condition: SwitchCondition::UnsetOrEmpty,
657 word: "foo bar".parse().unwrap(),
658 };
659 let (unquoted, is_quoted) = switch.unquote();
660 assert_eq!(unquoted, ":-foo bar");
661 assert_eq!(is_quoted, false);
662
663 let switch = Switch {
664 action: SwitchAction::Error,
665 condition: SwitchCondition::Unset,
666 word: r"e\r\ror".parse().unwrap(),
667 };
668 let (unquoted, is_quoted) = switch.unquote();
669 assert_eq!(unquoted, "?error");
670 assert_eq!(is_quoted, true);
671 }
672
673 #[test]
674 fn trim_unquote() {
675 let trim = Trim {
676 side: TrimSide::Prefix,
677 length: TrimLength::Shortest,
678 pattern: "".parse().unwrap(),
679 };
680 let (unquoted, is_quoted) = trim.unquote();
681 assert_eq!(unquoted, "#");
682 assert_eq!(is_quoted, false);
683
684 let trim = Trim {
685 side: TrimSide::Prefix,
686 length: TrimLength::Longest,
687 pattern: "'yes'".parse().unwrap(),
688 };
689 let (unquoted, is_quoted) = trim.unquote();
690 assert_eq!(unquoted, "##yes");
691 assert_eq!(is_quoted, true);
692
693 let trim = Trim {
694 side: TrimSide::Suffix,
695 length: TrimLength::Shortest,
696 pattern: r"\no".parse().unwrap(),
697 };
698 let (unquoted, is_quoted) = trim.unquote();
699 assert_eq!(unquoted, "%no");
700 assert_eq!(is_quoted, true);
701
702 let trim = Trim {
703 side: TrimSide::Suffix,
704 length: TrimLength::Longest,
705 pattern: "?".parse().unwrap(),
706 };
707 let (unquoted, is_quoted) = trim.unquote();
708 assert_eq!(unquoted, "%%?");
709 assert_eq!(is_quoted, false);
710 }
711
712 #[test]
713 fn braced_param_unquote() {
714 let param = BracedParam {
715 param: Param::variable("foo"),
716 modifier: Modifier::None,
717 location: Location::dummy(""),
718 };
719 let (unquoted, is_quoted) = param.unquote();
720 assert_eq!(unquoted, "${foo}");
721 assert_eq!(is_quoted, false);
722
723 let param = BracedParam {
724 modifier: Modifier::Length,
725 ..param
726 };
727 let (unquoted, is_quoted) = param.unquote();
728 assert_eq!(unquoted, "${#foo}");
729 assert_eq!(is_quoted, false);
730
731 let switch = Switch {
732 action: SwitchAction::Assign,
733 condition: SwitchCondition::UnsetOrEmpty,
734 word: "'bar'".parse().unwrap(),
735 };
736 let param = BracedParam {
737 modifier: Modifier::Switch(switch),
738 ..param
739 };
740 let (unquoted, is_quoted) = param.unquote();
741 assert_eq!(unquoted, "${foo:=bar}");
742 assert_eq!(is_quoted, true);
743
744 let trim = Trim {
745 side: TrimSide::Suffix,
746 length: TrimLength::Shortest,
747 pattern: "baz' 'bar".parse().unwrap(),
748 };
749 let param = BracedParam {
750 modifier: Modifier::Trim(trim),
751 ..param
752 };
753 let (unquoted, is_quoted) = param.unquote();
754 assert_eq!(unquoted, "${foo%baz bar}");
755 assert_eq!(is_quoted, true);
756 }
757
758 #[test]
759 fn backquote_unit_unquote() {
760 let literal = BackquoteUnit::Literal('A');
761 let (unquoted, is_quoted) = literal.unquote();
762 assert_eq!(unquoted, "A");
763 assert_eq!(is_quoted, false);
764
765 let backslashed = BackquoteUnit::Backslashed('X');
766 let (unquoted, is_quoted) = backslashed.unquote();
767 assert_eq!(unquoted, "X");
768 assert_eq!(is_quoted, true);
769 }
770
771 #[test]
772 fn text_from_literal_chars() {
773 let text = Text::from_literal_chars(['a', '1'].iter().copied());
774 assert_eq!(text.0, [Literal('a'), Literal('1')]);
775 }
776
777 #[test]
778 fn text_unquote_without_quotes() {
779 let empty = Text(vec![]);
780 let (unquoted, is_quoted) = empty.unquote();
781 assert_eq!(unquoted, "");
782 assert_eq!(is_quoted, false);
783
784 let nonempty = Text(vec![
785 Literal('W'),
786 RawParam {
787 param: Param::variable("X"),
788 location: Location::dummy(""),
789 },
790 CommandSubst {
791 content: "Y".into(),
792 location: Location::dummy(""),
793 },
794 Backquote {
795 content: vec![BackquoteUnit::Literal('Z')],
796 location: Location::dummy(""),
797 },
798 Arith {
799 content: Text(vec![Literal('0')]),
800 location: Location::dummy(""),
801 },
802 ]);
803 let (unquoted, is_quoted) = nonempty.unquote();
804 assert_eq!(unquoted, "W$X$(Y)`Z`$((0))");
805 assert_eq!(is_quoted, false);
806 }
807
808 #[test]
809 fn text_unquote_with_quotes() {
810 let quoted = Text(vec![
811 Literal('a'),
812 Backslashed('b'),
813 Literal('c'),
814 Arith {
815 content: Text(vec![Literal('d')]),
816 location: Location::dummy(""),
817 },
818 Literal('e'),
819 ]);
820 let (unquoted, is_quoted) = quoted.unquote();
821 assert_eq!(unquoted, "abc$((d))e");
822 assert_eq!(is_quoted, true);
823
824 let content = vec![BackquoteUnit::Backslashed('X')];
825 let location = Location::dummy("");
826 let quoted = Text(vec![Backquote { content, location }]);
827 let (unquoted, is_quoted) = quoted.unquote();
828 assert_eq!(unquoted, "`X`");
829 assert_eq!(is_quoted, true);
830
831 let content = Text(vec![Backslashed('X')]);
832 let location = Location::dummy("");
833 let quoted = Text(vec![Arith { content, location }]);
834 let (unquoted, is_quoted) = quoted.unquote();
835 assert_eq!(unquoted, "$((X))");
836 assert_eq!(is_quoted, true);
837 }
838
839 #[test]
840 fn text_to_string_if_literal_success() {
841 let empty = Text(vec![]);
842 let s = empty.to_string_if_literal().unwrap();
843 assert_eq!(s, "");
844
845 let nonempty = Text(vec![Literal('f'), Literal('o'), Literal('o')]);
846 let s = nonempty.to_string_if_literal().unwrap();
847 assert_eq!(s, "foo");
848 }
849
850 #[test]
851 fn text_to_string_if_literal_failure() {
852 let backslashed = Text(vec![Backslashed('a')]);
853 assert_eq!(backslashed.to_string_if_literal(), None);
854 }
855
856 #[test]
857 fn escape_unit_unquote() {
858 assert_eq!(EscapeUnit::Literal('A').unquote(), ("A".to_string(), false));
859 assert_eq!(EscapeUnit::DoubleQuote.unquote(), ("\"".to_string(), true));
860 assert_eq!(EscapeUnit::SingleQuote.unquote(), ("'".to_string(), true));
861 assert_eq!(EscapeUnit::Backslash.unquote(), ("\\".to_string(), true));
862 assert_eq!(EscapeUnit::Question.unquote(), ("?".to_string(), true));
863 assert_eq!(EscapeUnit::Alert.unquote(), ("\x07".to_string(), true));
864 assert_eq!(EscapeUnit::Backspace.unquote(), ("\x08".to_string(), true));
865 assert_eq!(EscapeUnit::Escape.unquote(), ("\x1B".to_string(), true));
866 assert_eq!(EscapeUnit::FormFeed.unquote(), ("\x0C".to_string(), true));
867 assert_eq!(EscapeUnit::Newline.unquote(), ("\n".to_string(), true));
868 assert_eq!(
869 EscapeUnit::CarriageReturn.unquote(),
870 ("\r".to_string(), true)
871 );
872 assert_eq!(EscapeUnit::Tab.unquote(), ("\t".to_string(), true));
873 assert_eq!(
874 EscapeUnit::VerticalTab.unquote(),
875 ("\x0B".to_string(), true)
876 );
877 assert_eq!(
878 EscapeUnit::Control(0x01).unquote(),
879 ("\x01".to_string(), true)
880 );
881 assert_eq!(
882 EscapeUnit::Control(0x1E).unquote(),
883 ("\x1E".to_string(), true)
884 );
885 assert_eq!(
886 EscapeUnit::Control(0x7F).unquote(),
887 ("\x7F".to_string(), true)
888 );
889 assert_eq!(EscapeUnit::Octal(0o123).unquote(), ("S".to_string(), true));
890 assert_eq!(EscapeUnit::Hex(0x41).unquote(), ("A".to_string(), true));
891 assert_eq!(
892 EscapeUnit::Unicode('🦀').unquote(),
893 ("🦀".to_string(), true)
894 );
895 }
896
897 #[test]
898 fn word_unquote() {
899 let word = Word::from_str(r#"a\b'c'"#).unwrap();
902 let (unquoted, is_quoted) = word.unquote();
903 assert_eq!(unquoted, "abc");
904 assert_eq!(is_quoted, true);
905
906 let word = Word::from_str("abc").unwrap();
907 let (unquoted, is_quoted) = word.unquote();
908 assert_eq!(unquoted, "abc");
909 assert_eq!(is_quoted, false);
910 }
911
912 #[test]
913 fn word_unit_unquote_unquoted() {
914 let unit = Unquoted(Literal('a'));
916 let (unquoted, is_quoted) = unit.unquote();
917 assert_eq!(unquoted, "a");
918 assert_eq!(is_quoted, false);
919
920 let unit = Unquoted(Backslashed('a'));
921 let (unquoted, is_quoted) = unit.unquote();
922 assert_eq!(unquoted, "a");
923 assert_eq!(is_quoted, true);
924 }
925
926 #[test]
927 fn word_unit_unquote_single_quote() {
928 let unit = SingleQuote(String::new());
930 let (unquoted, is_quoted) = unit.unquote();
931 assert_eq!(unquoted, "");
932 assert_eq!(is_quoted, true);
933
934 let unit = SingleQuote(r#"a"b\c"#.to_string());
935 let (unquoted, is_quoted) = unit.unquote();
936 assert_eq!(unquoted, r#"a"b\c"#);
937 assert_eq!(is_quoted, true);
938 }
939
940 #[test]
941 fn word_unit_unquote_double_quote() {
942 let unit = DoubleQuote(Text(vec![Literal('E'), Literal('O'), Literal('F')]));
945 let (unquoted, is_quoted) = unit.unquote();
946 assert_eq!(unquoted, "EOF");
947 assert_eq!(is_quoted, true);
948
949 let unit = DoubleQuote(Text(vec![RawParam {
952 param: Param::variable("foo"),
953 location: Location::dummy(""),
954 }]));
955 let (unquoted, is_quoted) = unit.unquote();
956 assert_eq!(unquoted, "$foo");
957 assert_eq!(is_quoted, true);
958 }
959
960 #[test]
961 fn word_unit_unquote_dollar_single_quote() {
962 let unit = DollarSingleQuote(EscapedString(vec![
965 EscapeUnit::Literal('E'),
966 EscapeUnit::Literal('O'),
967 EscapeUnit::Literal('F'),
968 ]));
969 let (unquoted, is_quoted) = unit.unquote();
970 assert_eq!(unquoted, "EOF");
971 assert_eq!(is_quoted, true);
972
973 let unit = DollarSingleQuote(EscapedString(vec![
974 EscapeUnit::Literal('a'),
975 EscapeUnit::Backslash,
976 ]));
977 let (unquoted, is_quoted) = unit.unquote();
978 assert_eq!(unquoted, r"a\");
979 assert_eq!(is_quoted, true);
980 }
981
982 #[test]
983 fn word_unit_unquote_tilde() {
984 let unit = Tilde {
986 name: String::new(),
987 followed_by_slash: false,
988 };
989 let (unquoted, is_quoted) = unit.unquote();
990 assert_eq!(unquoted, "~");
991 assert_eq!(is_quoted, false);
992
993 let unit = Tilde {
994 name: "foo".to_string(),
995 followed_by_slash: true,
996 };
997 let (unquoted, is_quoted) = unit.unquote();
998 assert_eq!(unquoted, "~foo");
999 assert_eq!(is_quoted, false);
1000 }
1001
1002 #[test]
1003 fn word_to_string_if_literal_success() {
1004 let empty = Word::from_str("").unwrap();
1005 let s = empty.to_string_if_literal().unwrap();
1006 assert_eq!(s, "");
1007
1008 let nonempty = Word::from_str("~foo").unwrap();
1009 let s = nonempty.to_string_if_literal().unwrap();
1010 assert_eq!(s, "~foo");
1011 }
1012
1013 #[test]
1014 fn word_to_string_if_literal_failure() {
1015 let location = Location::dummy("foo");
1016 let backslashed = Unquoted(Backslashed('?'));
1017 let word = Word {
1018 units: vec![backslashed],
1019 location,
1020 };
1021 assert_eq!(word.to_string_if_literal(), None);
1022
1023 let word = Word {
1024 units: vec![Tilde {
1025 name: "foo".to_string(),
1026 followed_by_slash: false,
1027 }],
1028 ..word
1029 };
1030 assert_eq!(word.to_string_if_literal(), None);
1031 }
1032
1033 #[test]
1034 fn assign_try_from_word_without_equal() {
1035 let word = Word::from_str("foo").unwrap();
1036 let result = Assign::try_from(word.clone());
1037 assert_eq!(result.unwrap_err(), word);
1038 }
1039
1040 #[test]
1041 fn assign_try_from_word_with_empty_name() {
1042 let word = Word::from_str("=foo").unwrap();
1043 let result = Assign::try_from(word.clone());
1044 assert_eq!(result.unwrap_err(), word);
1045 }
1046
1047 #[test]
1048 fn assign_try_from_word_with_non_literal_name() {
1049 let mut word = Word::from_str("night=foo").unwrap();
1050 word.units.insert(0, Unquoted(Backslashed('k')));
1051 let result = Assign::try_from(word.clone());
1052 assert_eq!(result.unwrap_err(), word);
1053 }
1054
1055 #[test]
1056 fn assign_try_from_word_with_literal_name() {
1057 let word = Word::from_str("night=foo").unwrap();
1058 let location = word.location.clone();
1059 let assign = Assign::try_from(word).unwrap();
1060 assert_eq!(assign.name, "night");
1061 assert_matches!(assign.value, Scalar(value) => {
1062 assert_eq!(value.to_string(), "foo");
1063 assert_eq!(value.location, location);
1064 });
1065 assert_eq!(assign.location, location);
1066 }
1067
1068 #[test]
1069 fn assign_try_from_word_tilde() {
1070 let word = Word::from_str("a=~:~b").unwrap();
1071 let assign = Assign::try_from(word).unwrap();
1072 assert_matches!(assign.value, Scalar(value) => {
1073 assert_eq!(
1074 value.units,
1075 [
1076 WordUnit::Tilde{
1077 name: "".to_string(),
1078 followed_by_slash: false,
1079 },
1080 WordUnit::Unquoted(TextUnit::Literal(':')),
1081 WordUnit::Tilde {
1082 name: "b".to_string(),
1083 followed_by_slash: false,
1084 },
1085 ]
1086 );
1087 });
1088 }
1089
1090 #[test]
1091 fn redir_op_conversions() {
1092 use RedirOp::*;
1093 for op in &[
1094 FileIn,
1095 FileInOut,
1096 FileOut,
1097 FileAppend,
1098 FileClobber,
1099 FdIn,
1100 FdOut,
1101 Pipe,
1102 String,
1103 ] {
1104 let op2 = RedirOp::try_from(Operator::from(*op));
1105 assert_eq!(op2, Ok(*op));
1106 }
1107 }
1108
1109 #[test]
1110 fn case_continuation_conversions() {
1111 use CaseContinuation::*;
1112 for cc in &[Break, FallThrough, Continue] {
1113 let cc2 = CaseContinuation::try_from(Operator::from(*cc));
1114 assert_eq!(cc2, Ok(*cc));
1115 }
1116 assert_eq!(
1117 CaseContinuation::try_from(Operator::SemicolonSemicolonAnd),
1118 Ok(Continue)
1119 );
1120 }
1121
1122 #[test]
1123 fn and_or_conversions() {
1124 for op in &[AndOr::AndThen, AndOr::OrElse] {
1125 let op2 = AndOr::try_from(Operator::from(*op));
1126 assert_eq!(op2, Ok(*op));
1127 }
1128 }
1129}