1#![allow(dead_code)]
7
8use crate::{
9 Name,
10 span::{Position, Span, TextRange},
11};
12use std::{
13 borrow::Cow,
14 fmt,
15 ops::{Deref, DerefMut},
16};
17
18fn assert_string_write(result: fmt::Result) {
19 if result.is_err() {
20 unreachable!("writing into a String should not fail");
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct SourceText {
28 span: Span,
29 cooked: Option<Box<str>>,
30}
31
32impl SourceText {
33 pub fn source(span: Span) -> Self {
34 Self { span, cooked: None }
35 }
36
37 pub fn cooked(span: Span, text: impl Into<Box<str>>) -> Self {
38 Self {
39 span,
40 cooked: Some(text.into()),
41 }
42 }
43
44 pub fn span(&self) -> Span {
45 self.span
46 }
47
48 pub fn slice<'a>(&'a self, source: &'a str) -> &'a str {
49 self.cooked
50 .as_deref()
51 .unwrap_or_else(|| self.span.slice(source))
52 }
53
54 pub fn is_source_backed(&self) -> bool {
55 self.cooked.is_none()
56 }
57
58 pub fn rebased(&mut self, base: Position) {
59 self.span = self.span.rebased(base);
60 }
61}
62
63impl From<Span> for SourceText {
64 fn from(span: Span) -> Self {
65 Self::source(span)
66 }
67}
68
69impl From<&str> for SourceText {
70 fn from(value: &str) -> Self {
71 Self::cooked(Span::new(), value)
72 }
73}
74
75impl From<String> for SourceText {
76 fn from(value: String) -> Self {
77 Self::cooked(Span::new(), value)
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum LiteralText {
87 Source,
88 Owned(Box<str>),
89 CookedSource(Box<str>),
90}
91
92impl LiteralText {
93 pub fn source() -> Self {
94 Self::Source
95 }
96
97 pub fn owned(text: impl Into<Box<str>>) -> Self {
98 Self::Owned(text.into())
99 }
100
101 pub fn cooked_source(text: impl Into<Box<str>>) -> Self {
102 Self::CookedSource(text.into())
103 }
104
105 pub fn as_str<'a>(&'a self, source: &'a str, span: Span) -> &'a str {
106 match self {
107 Self::Source => span.slice(source),
108 Self::Owned(text) | Self::CookedSource(text) => text.as_ref(),
109 }
110 }
111
112 pub fn syntax_str<'a>(&'a self, source: &'a str, span: Span) -> &'a str {
113 match self {
114 Self::Source | Self::CookedSource(_) => span.slice(source),
115 Self::Owned(text) => text.as_ref(),
116 }
117 }
118
119 pub fn eq_str(&self, source: &str, span: Span, other: &str) -> bool {
120 self.as_str(source, span) == other
121 }
122
123 pub fn is_source_backed(&self) -> bool {
124 matches!(self, Self::Source | Self::CookedSource(_))
125 }
126
127 pub fn is_empty(&self) -> bool {
128 matches!(self, Self::Owned(text) | Self::CookedSource(text) if text.is_empty())
129 }
130}
131
132impl From<&str> for LiteralText {
133 fn from(value: &str) -> Self {
134 Self::owned(value)
135 }
136}
137
138impl From<String> for LiteralText {
139 fn from(value: String) -> Self {
140 Self::owned(value)
141 }
142}
143
144impl PartialEq<str> for LiteralText {
145 fn eq(&self, other: &str) -> bool {
146 matches!(self, Self::Owned(text) | Self::CookedSource(text) if text.as_ref() == other)
147 }
148}
149
150impl PartialEq<&str> for LiteralText {
151 fn eq(&self, other: &&str) -> bool {
152 self == *other
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct Comment {
162 pub range: TextRange,
163}
164
165#[derive(Debug, Clone)]
167pub struct File {
168 pub body: StmtSeq,
169 pub span: Span,
171}
172
173#[derive(Debug, Clone)]
175pub struct StmtSeq {
176 pub leading_comments: Vec<Comment>,
178 pub stmts: Vec<Stmt>,
180 pub trailing_comments: Vec<Comment>,
182 pub span: Span,
184}
185
186impl StmtSeq {
187 pub fn len(&self) -> usize {
188 self.stmts.len()
189 }
190
191 pub fn is_empty(&self) -> bool {
192 self.stmts.is_empty()
193 }
194
195 pub fn as_slice(&self) -> &[Stmt] {
196 &self.stmts
197 }
198
199 pub fn as_mut_slice(&mut self) -> &mut [Stmt] {
200 &mut self.stmts
201 }
202
203 pub fn iter(&self) -> std::slice::Iter<'_, Stmt> {
204 self.stmts.iter()
205 }
206
207 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Stmt> {
208 self.stmts.iter_mut()
209 }
210
211 pub fn first(&self) -> Option<&Stmt> {
212 self.stmts.first()
213 }
214
215 pub fn last(&self) -> Option<&Stmt> {
216 self.stmts.last()
217 }
218}
219
220impl std::ops::Index<usize> for StmtSeq {
221 type Output = Stmt;
222
223 fn index(&self, index: usize) -> &Self::Output {
224 &self.stmts[index]
225 }
226}
227
228impl std::ops::IndexMut<usize> for StmtSeq {
229 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
230 &mut self.stmts[index]
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum StmtTerminator {
237 Semicolon,
238 Background(BackgroundOperator),
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum BackgroundOperator {
244 Plain,
245 Pipe,
246 Bang,
247}
248
249#[derive(Debug, Clone)]
251pub struct Stmt {
252 pub leading_comments: Vec<Comment>,
254 pub command: Command,
256 pub negated: bool,
258 pub redirects: Box<[Redirect]>,
260 pub terminator: Option<StmtTerminator>,
262 pub terminator_span: Option<Span>,
264 pub inline_comment: Option<Comment>,
266 pub span: Span,
268}
269
270#[derive(Debug, Clone)]
272#[allow(clippy::large_enum_variant)]
273pub enum Command {
274 Simple(SimpleCommand),
276
277 Builtin(BuiltinCommand),
279
280 Decl(DeclClause),
282
283 Binary(BinaryCommand),
285
286 Compound(CompoundCommand),
288
289 Function(FunctionDef),
291
292 AnonymousFunction(AnonymousFunctionCommand),
294}
295
296#[derive(Debug, Clone)]
298pub struct SimpleCommand {
299 pub name: Word,
301 pub args: Vec<Word>,
303 pub assignments: Box<[Assignment]>,
305 pub span: Span,
307}
308
309#[derive(Debug, Clone)]
311pub struct DeclClause {
312 pub variant: Name,
314 pub variant_span: Span,
316 pub operands: Vec<DeclOperand>,
318 pub assignments: Box<[Assignment]>,
320 pub span: Span,
322}
323
324#[derive(Debug, Clone)]
326pub enum DeclOperand {
327 Flag(Word),
329 Name(VarRef),
331 Assignment(Assignment),
333 Dynamic(Word),
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum SubscriptInterpretation {
340 Indexed,
341 Associative,
342 Contextual,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum SubscriptKind {
348 Ordinary,
349 Selector(SubscriptSelector),
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum SubscriptSelector {
355 At,
356 Star,
357}
358
359impl SubscriptSelector {
360 pub const fn as_char(self) -> char {
361 match self {
362 Self::At => '@',
363 Self::Star => '*',
364 }
365 }
366}
367
368#[derive(Debug, Clone)]
370pub struct Subscript {
371 pub text: SourceText,
372 pub raw: Option<SourceText>,
374 pub kind: SubscriptKind,
375 pub interpretation: SubscriptInterpretation,
376 pub word_ast: Option<Word>,
378 pub arithmetic_ast: Option<ArithmeticExprNode>,
380}
381
382impl Subscript {
383 pub fn span(&self) -> Span {
384 self.text.span()
385 }
386
387 pub fn syntax_source_text(&self) -> &SourceText {
388 self.raw.as_ref().unwrap_or(&self.text)
389 }
390
391 pub fn syntax_text<'a>(&'a self, source: &'a str) -> &'a str {
392 self.syntax_source_text().slice(source)
393 }
394
395 pub fn is_array_selector(&self) -> bool {
396 matches!(self.kind, SubscriptKind::Selector(_))
397 }
398
399 pub fn selector(&self) -> Option<SubscriptSelector> {
400 match self.kind {
401 SubscriptKind::Ordinary => None,
402 SubscriptKind::Selector(selector) => Some(selector),
403 }
404 }
405
406 pub fn is_source_backed(&self) -> bool {
407 self.syntax_source_text().is_source_backed()
408 }
409
410 pub fn word_ast(&self) -> Option<&Word> {
411 self.word_ast.as_ref()
412 }
413}
414
415#[derive(Debug, Clone)]
417pub struct VarRef {
418 pub name: Name,
419 pub name_span: Span,
420 pub subscript: Option<Subscript>,
421 pub span: Span,
422}
423
424impl VarRef {
425 pub fn has_array_selector(&self) -> bool {
426 self.subscript
427 .as_ref()
428 .is_some_and(Subscript::is_array_selector)
429 }
430
431 pub fn is_source_backed(&self) -> bool {
432 self.subscript
433 .as_ref()
434 .is_none_or(Subscript::is_source_backed)
435 }
436}
437
438#[derive(Debug, Clone)]
440pub enum BuiltinCommand {
441 Break(BreakCommand),
443 Continue(ContinueCommand),
445 Return(ReturnCommand),
447 Exit(ExitCommand),
449}
450
451#[derive(Debug, Clone)]
453pub struct BreakCommand {
454 pub depth: Option<Word>,
456 pub extra_args: Vec<Word>,
458 pub assignments: Box<[Assignment]>,
460 pub span: Span,
462}
463
464#[derive(Debug, Clone)]
466pub struct ContinueCommand {
467 pub depth: Option<Word>,
469 pub extra_args: Vec<Word>,
471 pub assignments: Box<[Assignment]>,
473 pub span: Span,
475}
476
477#[derive(Debug, Clone)]
479pub struct ReturnCommand {
480 pub code: Option<Word>,
482 pub extra_args: Vec<Word>,
484 pub assignments: Box<[Assignment]>,
486 pub span: Span,
488}
489
490#[derive(Debug, Clone)]
492pub struct ExitCommand {
493 pub code: Option<Word>,
495 pub extra_args: Vec<Word>,
497 pub assignments: Box<[Assignment]>,
499 pub span: Span,
501}
502
503#[derive(Debug, Clone)]
505pub struct BinaryCommand {
506 pub left: Box<Stmt>,
507 pub op: BinaryOp,
508 pub op_span: Span,
509 pub right: Box<Stmt>,
510 pub span: Span,
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum BinaryOp {
516 And,
517 Or,
518 Pipe,
519 PipeAll,
520}
521
522#[derive(Debug, Clone)]
524pub enum CompoundCommand {
525 If(IfCommand),
527 For(ForCommand),
529 Repeat(RepeatCommand),
531 Foreach(ForeachCommand),
533 ArithmeticFor(Box<ArithmeticForCommand>),
535 While(WhileCommand),
537 Until(UntilCommand),
539 Case(CaseCommand),
541 Select(SelectCommand),
543 Subshell(StmtSeq),
545 BraceGroup(StmtSeq),
547 Arithmetic(ArithmeticCommand),
549 Time(TimeCommand),
551 Conditional(ConditionalCommand),
553 Coproc(CoprocCommand),
555 Always(AlwaysCommand),
557}
558
559#[derive(Debug, Clone)]
565pub struct CoprocCommand {
566 pub name: Name,
568 pub name_span: Option<Span>,
570 pub body: Box<Stmt>,
572 pub span: Span,
574}
575
576#[derive(Debug, Clone)]
578pub struct AlwaysCommand {
579 pub body: StmtSeq,
580 pub always_body: StmtSeq,
581 pub span: Span,
582}
583
584#[derive(Debug, Clone)]
590pub struct TimeCommand {
591 pub posix_format: bool,
593 pub command: Option<Box<Stmt>>,
595 pub span: Span,
597}
598
599#[derive(Debug, Clone)]
601pub struct ConditionalCommand {
602 pub expression: ConditionalExpr,
604 pub span: Span,
606 pub left_bracket_span: Span,
608 pub right_bracket_span: Span,
610}
611
612#[derive(Debug, Clone)]
614pub enum ConditionalExpr {
615 Binary(ConditionalBinaryExpr),
616 Unary(ConditionalUnaryExpr),
617 Parenthesized(ConditionalParenExpr),
618 Word(Word),
619 Pattern(Pattern),
620 Regex(Word),
621 VarRef(Box<VarRef>),
622}
623
624impl ConditionalExpr {
625 pub fn span(&self) -> Span {
627 match self {
628 Self::Binary(expr) => expr.span(),
629 Self::Unary(expr) => expr.span(),
630 Self::Parenthesized(expr) => expr.span(),
631 Self::Word(word) | Self::Regex(word) => word.span,
632 Self::Pattern(pattern) => pattern.span,
633 Self::VarRef(var_ref) => var_ref.span,
634 }
635 }
636}
637
638#[derive(Debug, Clone)]
640pub struct ConditionalBinaryExpr {
641 pub left: Box<ConditionalExpr>,
642 pub op: ConditionalBinaryOp,
643 pub op_span: Span,
644 pub right: Box<ConditionalExpr>,
645}
646
647impl ConditionalBinaryExpr {
648 pub fn span(&self) -> Span {
649 self.left.span().merge(self.right.span())
650 }
651}
652
653#[derive(Debug, Clone)]
655pub struct ConditionalUnaryExpr {
656 pub op: ConditionalUnaryOp,
657 pub op_span: Span,
658 pub expr: Box<ConditionalExpr>,
659}
660
661impl ConditionalUnaryExpr {
662 pub fn span(&self) -> Span {
663 self.op_span.merge(self.expr.span())
664 }
665}
666
667#[derive(Debug, Clone)]
669pub struct ConditionalParenExpr {
670 pub left_paren_span: Span,
671 pub expr: Box<ConditionalExpr>,
672 pub right_paren_span: Span,
673}
674
675impl ConditionalParenExpr {
676 pub fn span(&self) -> Span {
677 self.left_paren_span.merge(self.right_paren_span)
678 }
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
683pub enum ConditionalBinaryOp {
684 RegexMatch,
685 NewerThan,
686 OlderThan,
687 SameFile,
688 ArithmeticEq,
689 ArithmeticNe,
690 ArithmeticLe,
691 ArithmeticGe,
692 ArithmeticLt,
693 ArithmeticGt,
694 And,
695 Or,
696 PatternEqShort,
697 PatternEq,
698 PatternNe,
699 LexicalBefore,
700 LexicalAfter,
701}
702
703impl ConditionalBinaryOp {
704 pub fn as_str(self) -> &'static str {
705 match self {
706 Self::RegexMatch => "=~",
707 Self::NewerThan => "-nt",
708 Self::OlderThan => "-ot",
709 Self::SameFile => "-ef",
710 Self::ArithmeticEq => "-eq",
711 Self::ArithmeticNe => "-ne",
712 Self::ArithmeticLe => "-le",
713 Self::ArithmeticGe => "-ge",
714 Self::ArithmeticLt => "-lt",
715 Self::ArithmeticGt => "-gt",
716 Self::And => "&&",
717 Self::Or => "||",
718 Self::PatternEqShort => "=",
719 Self::PatternEq => "==",
720 Self::PatternNe => "!=",
721 Self::LexicalBefore => "<",
722 Self::LexicalAfter => ">",
723 }
724 }
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
729pub enum ConditionalUnaryOp {
730 Exists,
731 RegularFile,
732 Directory,
733 CharacterSpecial,
734 BlockSpecial,
735 NamedPipe,
736 Socket,
737 Symlink,
738 Sticky,
739 SetGroupId,
740 SetUserId,
741 GroupOwned,
742 UserOwned,
743 Modified,
744 Readable,
745 Writable,
746 Executable,
747 NonEmptyFile,
748 FdTerminal,
749 EmptyString,
750 NonEmptyString,
751 OptionSet,
752 VariableSet,
753 ReferenceVariable,
754 Not,
755}
756
757impl ConditionalUnaryOp {
758 pub fn as_str(self) -> &'static str {
759 match self {
760 Self::Exists => "-e",
761 Self::RegularFile => "-f",
762 Self::Directory => "-d",
763 Self::CharacterSpecial => "-c",
764 Self::BlockSpecial => "-b",
765 Self::NamedPipe => "-p",
766 Self::Socket => "-S",
767 Self::Symlink => "-L",
768 Self::Sticky => "-k",
769 Self::SetGroupId => "-g",
770 Self::SetUserId => "-u",
771 Self::GroupOwned => "-G",
772 Self::UserOwned => "-O",
773 Self::Modified => "-N",
774 Self::Readable => "-r",
775 Self::Writable => "-w",
776 Self::Executable => "-x",
777 Self::NonEmptyFile => "-s",
778 Self::FdTerminal => "-t",
779 Self::EmptyString => "-z",
780 Self::NonEmptyString => "-n",
781 Self::OptionSet => "-o",
782 Self::VariableSet => "-v",
783 Self::ReferenceVariable => "-R",
784 Self::Not => "!",
785 }
786 }
787}
788
789#[derive(Debug, Clone)]
791pub struct IfCommand {
792 pub condition: StmtSeq,
793 pub then_branch: StmtSeq,
794 pub elif_branches: Vec<(StmtSeq, StmtSeq)>,
795 pub else_branch: Option<StmtSeq>,
796 pub syntax: IfSyntax,
797 pub span: Span,
799}
800
801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
803pub enum IfSyntax {
804 ThenFi {
805 then_span: Span,
806 fi_span: Span,
807 },
808 Brace {
809 left_brace_span: Span,
810 right_brace_span: Span,
811 },
812}
813
814#[derive(Debug, Clone)]
816pub struct ForCommand {
817 pub targets: Vec<ForTarget>,
818 pub words: Option<Vec<Word>>,
819 pub body: StmtSeq,
820 pub syntax: ForSyntax,
821 pub span: Span,
823}
824
825#[derive(Debug, Clone)]
827pub struct ForTarget {
828 pub word: Word,
830 pub name: Option<Name>,
832 pub span: Span,
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq)]
837pub enum ForSyntax {
838 InDoDone {
839 in_span: Option<Span>,
840 do_span: Span,
841 done_span: Span,
842 },
843 InDirect {
844 in_span: Option<Span>,
845 },
846 InBrace {
847 in_span: Option<Span>,
848 left_brace_span: Span,
849 right_brace_span: Span,
850 },
851 ParenDoDone {
852 left_paren_span: Span,
853 right_paren_span: Span,
854 do_span: Span,
855 done_span: Span,
856 },
857 ParenDirect {
858 left_paren_span: Span,
859 right_paren_span: Span,
860 },
861 ParenBrace {
862 left_paren_span: Span,
863 right_paren_span: Span,
864 left_brace_span: Span,
865 right_brace_span: Span,
866 },
867}
868
869#[derive(Debug, Clone)]
871pub struct RepeatCommand {
872 pub count: Word,
873 pub body: StmtSeq,
874 pub syntax: RepeatSyntax,
875 pub span: Span,
877}
878
879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881pub enum RepeatSyntax {
882 DoDone {
883 do_span: Span,
884 done_span: Span,
885 },
886 Direct,
887 Brace {
888 left_brace_span: Span,
889 right_brace_span: Span,
890 },
891}
892
893#[derive(Debug, Clone)]
895pub struct ForeachCommand {
896 pub variable: Name,
897 pub variable_span: Span,
898 pub words: Vec<Word>,
899 pub body: StmtSeq,
900 pub syntax: ForeachSyntax,
901 pub span: Span,
903}
904
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
907pub enum ForeachSyntax {
908 ParenBrace {
909 left_paren_span: Span,
910 right_paren_span: Span,
911 left_brace_span: Span,
912 right_brace_span: Span,
913 },
914 InDoDone {
915 in_span: Span,
916 do_span: Span,
917 done_span: Span,
918 },
919}
920
921#[derive(Debug, Clone)]
923pub struct SelectCommand {
924 pub variable: Name,
925 pub variable_span: Span,
926 pub words: Vec<Word>,
927 pub body: StmtSeq,
928 pub span: Span,
930}
931
932#[derive(Debug, Clone)]
934pub struct ArithmeticCommand {
935 pub span: Span,
936 pub left_paren_span: Span,
937 pub expr_span: Option<Span>,
938 pub expr_ast: Option<ArithmeticExprNode>,
940 pub right_paren_span: Span,
941}
942
943#[derive(Debug, Clone)]
945pub struct ArithmeticForCommand {
946 pub left_paren_span: Span,
947 pub init_span: Option<Span>,
948 pub init_ast: Option<ArithmeticExprNode>,
950 pub first_semicolon_span: Span,
951 pub condition_span: Option<Span>,
952 pub condition_ast: Option<ArithmeticExprNode>,
954 pub second_semicolon_span: Span,
955 pub step_span: Option<Span>,
956 pub step_ast: Option<ArithmeticExprNode>,
958 pub right_paren_span: Span,
959 pub body: StmtSeq,
961 pub span: Span,
963}
964
965#[derive(Debug, Clone)]
967pub struct ArithmeticExprNode {
968 pub kind: ArithmeticExpr,
969 pub span: Span,
970}
971
972impl ArithmeticExprNode {
973 pub fn new(kind: ArithmeticExpr, span: Span) -> Self {
974 Self { kind, span }
975 }
976}
977
978#[derive(Debug, Clone)]
980pub enum ArithmeticExpr {
981 Number(SourceText),
983 Variable(Name),
985 Indexed {
987 name: Name,
988 index: Box<ArithmeticExprNode>,
989 },
990 ShellWord(Word),
992 Parenthesized {
993 expression: Box<ArithmeticExprNode>,
994 },
995 Unary {
996 op: ArithmeticUnaryOp,
997 expr: Box<ArithmeticExprNode>,
998 },
999 Postfix {
1000 expr: Box<ArithmeticExprNode>,
1001 op: ArithmeticPostfixOp,
1002 },
1003 Binary {
1004 left: Box<ArithmeticExprNode>,
1005 op: ArithmeticBinaryOp,
1006 right: Box<ArithmeticExprNode>,
1007 },
1008 Conditional {
1009 condition: Box<ArithmeticExprNode>,
1010 then_expr: Box<ArithmeticExprNode>,
1011 else_expr: Box<ArithmeticExprNode>,
1012 },
1013 Assignment {
1014 target: ArithmeticLvalue,
1015 op: ArithmeticAssignOp,
1016 value: Box<ArithmeticExprNode>,
1017 },
1018}
1019
1020#[derive(Debug, Clone)]
1022pub enum ArithmeticLvalue {
1023 Variable(Name),
1024 Indexed {
1025 name: Name,
1026 index: Box<ArithmeticExprNode>,
1027 },
1028}
1029
1030#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1032pub enum ArithmeticUnaryOp {
1033 PreIncrement,
1034 PreDecrement,
1035 Plus,
1036 Minus,
1037 LogicalNot,
1038 BitwiseNot,
1039}
1040
1041#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1043pub enum ArithmeticPostfixOp {
1044 Increment,
1045 Decrement,
1046}
1047
1048#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1050pub enum ArithmeticBinaryOp {
1051 Comma,
1052 Power,
1053 Multiply,
1054 Divide,
1055 Modulo,
1056 Add,
1057 Subtract,
1058 ShiftLeft,
1059 ShiftRight,
1060 LessThan,
1061 LessThanOrEqual,
1062 GreaterThan,
1063 GreaterThanOrEqual,
1064 Equal,
1065 NotEqual,
1066 BitwiseAnd,
1067 BitwiseXor,
1068 BitwiseOr,
1069 LogicalAnd,
1070 LogicalOr,
1071}
1072
1073#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1075pub enum ArithmeticAssignOp {
1076 Assign,
1077 AddAssign,
1078 SubAssign,
1079 MulAssign,
1080 DivAssign,
1081 ModAssign,
1082 ShiftLeftAssign,
1083 ShiftRightAssign,
1084 AndAssign,
1085 XorAssign,
1086 OrAssign,
1087}
1088
1089#[derive(Debug, Clone)]
1091pub struct WhileCommand {
1092 pub condition: StmtSeq,
1093 pub body: StmtSeq,
1094 pub span: Span,
1096}
1097
1098#[derive(Debug, Clone)]
1100pub struct UntilCommand {
1101 pub condition: StmtSeq,
1102 pub body: StmtSeq,
1103 pub span: Span,
1105}
1106
1107#[derive(Debug, Clone)]
1109pub struct CaseCommand {
1110 pub word: Word,
1111 pub cases: Vec<CaseItem>,
1112 pub span: Span,
1114}
1115
1116#[derive(Debug, Clone, Copy, PartialEq)]
1118pub enum CaseTerminator {
1119 Break,
1121 FallThrough,
1123 Continue,
1125 ContinueMatching,
1127}
1128
1129#[derive(Debug, Clone)]
1131pub struct CaseItem {
1132 pub patterns: Vec<Pattern>,
1133 pub body: StmtSeq,
1134 pub terminator: CaseTerminator,
1135 pub terminator_span: Option<Span>,
1137}
1138
1139#[derive(Debug, Clone)]
1141pub struct FunctionHeaderEntry {
1142 pub word: Word,
1143 pub static_name: Option<Name>,
1144}
1145
1146impl FunctionHeaderEntry {
1147 pub fn static_name_span(&self) -> Option<Span> {
1148 self.static_name.as_ref().map(|_| self.word.span)
1149 }
1150}
1151
1152#[derive(Debug, Clone, Default)]
1154pub struct FunctionHeader {
1155 pub function_keyword_span: Option<Span>,
1156 pub entries: Vec<FunctionHeaderEntry>,
1157 pub trailing_parens_span: Option<Span>,
1158}
1159
1160impl FunctionHeader {
1161 pub fn uses_function_keyword(&self) -> bool {
1162 self.function_keyword_span.is_some()
1163 }
1164
1165 pub fn has_trailing_parens(&self) -> bool {
1166 self.trailing_parens_span.is_some()
1167 }
1168
1169 pub fn has_name_parens(&self) -> bool {
1170 self.has_trailing_parens()
1171 }
1172
1173 pub fn static_names(&self) -> impl Iterator<Item = &Name> + '_ {
1174 self.entries
1175 .iter()
1176 .filter_map(|entry| entry.static_name.as_ref())
1177 }
1178
1179 pub fn static_name_entries(&self) -> impl Iterator<Item = (&Name, Span)> + '_ {
1180 self.entries.iter().filter_map(|entry| {
1181 entry
1182 .static_name
1183 .as_ref()
1184 .map(|name| (name, entry.word.span))
1185 })
1186 }
1187
1188 pub fn span(&self) -> Span {
1189 let mut span = self.function_keyword_span.unwrap_or_default();
1190 for entry in &self.entries {
1191 span = merge_non_empty_span(span, entry.word.span);
1192 }
1193 if let Some(parens_span) = self.trailing_parens_span {
1194 span = merge_non_empty_span(span, parens_span);
1195 }
1196 span
1197 }
1198}
1199
1200#[derive(Debug, Clone)]
1202pub struct FunctionDef {
1203 pub header: FunctionHeader,
1204 pub body: Box<Stmt>,
1205 pub span: Span,
1207}
1208
1209impl FunctionDef {
1210 pub fn uses_function_keyword(&self) -> bool {
1211 self.header.uses_function_keyword()
1212 }
1213
1214 pub fn has_trailing_parens(&self) -> bool {
1215 self.header.has_trailing_parens()
1216 }
1217
1218 pub fn has_name_parens(&self) -> bool {
1219 self.has_trailing_parens()
1220 }
1221
1222 pub fn static_names(&self) -> impl Iterator<Item = &Name> + '_ {
1223 self.header.static_names()
1224 }
1225
1226 pub fn static_name_entries(&self) -> impl Iterator<Item = (&Name, Span)> + '_ {
1227 self.header.static_name_entries()
1228 }
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1233pub enum AnonymousFunctionSurface {
1234 FunctionKeyword { function_keyword_span: Span },
1235 Parens { parens_span: Span },
1236}
1237
1238impl AnonymousFunctionSurface {
1239 pub fn uses_function_keyword(&self) -> bool {
1240 matches!(self, Self::FunctionKeyword { .. })
1241 }
1242
1243 pub fn parens_span(self) -> Option<Span> {
1244 match self {
1245 Self::FunctionKeyword { .. } => None,
1246 Self::Parens { parens_span } => Some(parens_span),
1247 }
1248 }
1249}
1250
1251#[derive(Debug, Clone)]
1253pub struct AnonymousFunctionCommand {
1254 pub surface: AnonymousFunctionSurface,
1255 pub body: Box<Stmt>,
1256 pub args: Vec<Word>,
1257 pub span: Span,
1258}
1259
1260impl AnonymousFunctionCommand {
1261 pub fn uses_function_keyword(&self) -> bool {
1262 self.surface.uses_function_keyword()
1263 }
1264}
1265
1266fn merge_non_empty_span(current: Span, next: Span) -> Span {
1267 match (current == Span::new(), next == Span::new()) {
1268 (true, _) => next,
1269 (_, true) => current,
1270 (false, false) => current.merge(next),
1271 }
1272}
1273
1274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1276pub enum CommandSubstitutionSyntax {
1277 DollarParen,
1278 Backtick,
1279}
1280
1281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1283pub enum ArithmeticExpansionSyntax {
1284 DollarParenParen,
1285 LegacyBracket,
1286}
1287
1288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1290pub enum PrefixMatchKind {
1291 At,
1292 Star,
1293}
1294
1295impl PrefixMatchKind {
1296 pub const fn as_char(self) -> char {
1297 match self {
1298 Self::At => '@',
1299 Self::Star => '*',
1300 }
1301 }
1302}
1303
1304#[derive(Debug, Clone)]
1306pub struct ParameterExpansion {
1307 pub syntax: ParameterExpansionSyntax,
1308 pub span: Span,
1309 pub raw_body: SourceText,
1310}
1311
1312impl ParameterExpansion {
1313 pub fn is_zsh(&self) -> bool {
1314 matches!(self.syntax, ParameterExpansionSyntax::Zsh(_))
1315 }
1316
1317 pub fn bourne(&self) -> Option<&BourneParameterExpansion> {
1318 match &self.syntax {
1319 ParameterExpansionSyntax::Bourne(syntax) => Some(syntax),
1320 ParameterExpansionSyntax::Zsh(_) => None,
1321 }
1322 }
1323
1324 pub fn zsh(&self) -> Option<&ZshParameterExpansion> {
1325 match &self.syntax {
1326 ParameterExpansionSyntax::Bourne(_) => None,
1327 ParameterExpansionSyntax::Zsh(syntax) => Some(syntax),
1328 }
1329 }
1330}
1331
1332#[derive(Debug, Clone)]
1333#[allow(clippy::large_enum_variant)]
1334pub enum ParameterExpansionSyntax {
1335 Bourne(BourneParameterExpansion),
1336 Zsh(ZshParameterExpansion),
1337}
1338
1339#[derive(Debug, Clone)]
1340#[allow(clippy::large_enum_variant)]
1341pub enum BourneParameterExpansion {
1342 Access {
1343 reference: VarRef,
1344 },
1345 Length {
1346 reference: VarRef,
1347 },
1348 Indices {
1349 reference: VarRef,
1350 },
1351 Indirect {
1352 reference: VarRef,
1353 operator: Option<ParameterOp>,
1354 operand: Option<SourceText>,
1355 operand_word_ast: Option<Word>,
1356 colon_variant: bool,
1357 },
1358 PrefixMatch {
1359 prefix: Name,
1360 kind: PrefixMatchKind,
1361 },
1362 Slice {
1363 reference: VarRef,
1364 offset: SourceText,
1365 offset_ast: Option<ArithmeticExprNode>,
1366 offset_word_ast: Word,
1367 length: Option<SourceText>,
1368 length_ast: Option<ArithmeticExprNode>,
1369 length_word_ast: Option<Word>,
1370 },
1371 Operation {
1372 reference: VarRef,
1373 operator: ParameterOp,
1374 operand: Option<SourceText>,
1375 operand_word_ast: Option<Word>,
1376 colon_variant: bool,
1377 },
1378 Transformation {
1379 reference: VarRef,
1380 operator: char,
1381 },
1382}
1383
1384impl BourneParameterExpansion {
1385 pub fn operand_word_ast(&self) -> Option<&Word> {
1386 match self {
1387 Self::Indirect {
1388 operand_word_ast, ..
1389 }
1390 | Self::Operation {
1391 operand_word_ast, ..
1392 } => operand_word_ast.as_ref(),
1393 Self::Access { .. }
1394 | Self::Length { .. }
1395 | Self::Indices { .. }
1396 | Self::PrefixMatch { .. }
1397 | Self::Slice { .. }
1398 | Self::Transformation { .. } => None,
1399 }
1400 }
1401
1402 pub fn offset_word_ast(&self) -> Option<&Word> {
1403 match self {
1404 Self::Slice {
1405 offset_word_ast, ..
1406 } => Some(offset_word_ast),
1407 Self::Access { .. }
1408 | Self::Length { .. }
1409 | Self::Indices { .. }
1410 | Self::Indirect { .. }
1411 | Self::PrefixMatch { .. }
1412 | Self::Operation { .. }
1413 | Self::Transformation { .. } => None,
1414 }
1415 }
1416
1417 pub fn length_word_ast(&self) -> Option<&Word> {
1418 match self {
1419 Self::Slice {
1420 length_word_ast, ..
1421 } => length_word_ast.as_ref(),
1422 Self::Access { .. }
1423 | Self::Length { .. }
1424 | Self::Indices { .. }
1425 | Self::Indirect { .. }
1426 | Self::PrefixMatch { .. }
1427 | Self::Operation { .. }
1428 | Self::Transformation { .. } => None,
1429 }
1430 }
1431}
1432
1433#[derive(Debug, Clone)]
1434pub struct ZshParameterExpansion {
1435 pub target: ZshExpansionTarget,
1436 pub modifiers: Vec<ZshModifier>,
1437 pub length_prefix: Option<Span>,
1438 pub operation: Option<ZshExpansionOperation>,
1439}
1440
1441#[derive(Debug, Clone)]
1442#[allow(clippy::large_enum_variant)]
1443pub enum ZshExpansionTarget {
1444 Reference(VarRef),
1445 Nested(Box<ParameterExpansion>),
1446 Word(Word),
1447 Empty,
1448}
1449
1450#[derive(Debug, Clone)]
1451pub struct ZshModifier {
1452 pub name: char,
1453 pub argument: Option<SourceText>,
1454 pub argument_word_ast: Option<Word>,
1455 pub argument_delimiter: Option<char>,
1456 pub span: Span,
1457}
1458
1459impl ZshModifier {
1460 pub fn argument_word_ast(&self) -> Option<&Word> {
1461 self.argument_word_ast.as_ref()
1462 }
1463}
1464
1465#[derive(Debug, Clone)]
1466pub enum ZshExpansionOperation {
1467 PatternOperation {
1468 kind: ZshPatternOp,
1469 operand: SourceText,
1470 operand_word_ast: Word,
1471 },
1472 Defaulting {
1473 kind: ZshDefaultingOp,
1474 operand: SourceText,
1475 operand_word_ast: Word,
1476 colon_variant: bool,
1477 },
1478 TrimOperation {
1479 kind: ZshTrimOp,
1480 operand: SourceText,
1481 operand_word_ast: Word,
1482 },
1483 ReplacementOperation {
1484 kind: ZshReplacementOp,
1485 pattern: SourceText,
1486 pattern_word_ast: Word,
1487 replacement: Option<SourceText>,
1488 replacement_word_ast: Option<Word>,
1489 },
1490 Slice {
1491 offset: SourceText,
1492 offset_word_ast: Word,
1493 length: Option<SourceText>,
1494 length_word_ast: Option<Word>,
1495 },
1496 Unknown {
1497 text: SourceText,
1498 word_ast: Word,
1499 },
1500}
1501
1502impl ZshExpansionOperation {
1503 pub fn operand_word_ast(&self) -> Option<&Word> {
1504 match self {
1505 Self::PatternOperation {
1506 operand_word_ast, ..
1507 }
1508 | Self::Defaulting {
1509 operand_word_ast, ..
1510 }
1511 | Self::TrimOperation {
1512 operand_word_ast, ..
1513 } => Some(operand_word_ast),
1514 Self::ReplacementOperation { .. } | Self::Slice { .. } => None,
1515 Self::Unknown { word_ast, .. } => Some(word_ast),
1516 }
1517 }
1518
1519 pub fn pattern_word_ast(&self) -> Option<&Word> {
1520 match self {
1521 Self::ReplacementOperation {
1522 pattern_word_ast, ..
1523 } => Some(pattern_word_ast),
1524 Self::PatternOperation { .. }
1525 | Self::Defaulting { .. }
1526 | Self::TrimOperation { .. }
1527 | Self::Slice { .. }
1528 | Self::Unknown { .. } => None,
1529 }
1530 }
1531
1532 pub fn replacement_word_ast(&self) -> Option<&Word> {
1533 match self {
1534 Self::ReplacementOperation {
1535 replacement_word_ast,
1536 ..
1537 } => replacement_word_ast.as_ref(),
1538 Self::PatternOperation { .. }
1539 | Self::Defaulting { .. }
1540 | Self::TrimOperation { .. }
1541 | Self::Slice { .. }
1542 | Self::Unknown { .. } => None,
1543 }
1544 }
1545
1546 pub fn offset_word_ast(&self) -> Option<&Word> {
1547 match self {
1548 Self::Slice {
1549 offset_word_ast, ..
1550 } => Some(offset_word_ast),
1551 Self::PatternOperation { .. }
1552 | Self::Defaulting { .. }
1553 | Self::TrimOperation { .. }
1554 | Self::ReplacementOperation { .. }
1555 | Self::Unknown { .. } => None,
1556 }
1557 }
1558
1559 pub fn length_word_ast(&self) -> Option<&Word> {
1560 match self {
1561 Self::Slice {
1562 length_word_ast, ..
1563 } => length_word_ast.as_ref(),
1564 Self::PatternOperation { .. }
1565 | Self::Defaulting { .. }
1566 | Self::TrimOperation { .. }
1567 | Self::ReplacementOperation { .. }
1568 | Self::Unknown { .. } => None,
1569 }
1570 }
1571
1572 pub fn source_text(&self) -> Option<&SourceText> {
1573 match self {
1574 Self::PatternOperation { operand, .. }
1575 | Self::Defaulting { operand, .. }
1576 | Self::TrimOperation { operand, .. } => Some(operand),
1577 Self::ReplacementOperation { .. } | Self::Slice { .. } => None,
1578 Self::Unknown { text, .. } => Some(text),
1579 }
1580 }
1581}
1582
1583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1584pub enum ZshPatternOp {
1585 Filter,
1586}
1587
1588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1589pub enum ZshDefaultingOp {
1590 UseDefault,
1591 AssignDefault,
1592 UseReplacement,
1593 Error,
1594}
1595
1596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1597pub enum ZshTrimOp {
1598 RemovePrefixShort,
1599 RemovePrefixLong,
1600 RemoveSuffixShort,
1601 RemoveSuffixLong,
1602}
1603
1604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1605pub enum ZshReplacementOp {
1606 ReplaceFirst,
1607 ReplaceAll,
1608 ReplacePrefix,
1609 ReplaceSuffix,
1610}
1611
1612#[derive(Debug, Clone)]
1615pub struct ZshQualifiedGlob {
1616 pub span: Span,
1617 pub segments: Vec<ZshGlobSegment>,
1618 pub qualifiers: Option<ZshGlobQualifierGroup>,
1619}
1620
1621#[derive(Debug, Clone)]
1623pub enum ZshGlobSegment {
1624 Pattern(Pattern),
1625 InlineControl(ZshInlineGlobControl),
1626}
1627
1628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1630pub enum ZshInlineGlobControl {
1631 CaseInsensitive { span: Span },
1632 Backreferences { span: Span },
1633 StartAnchor { span: Span },
1634 EndAnchor { span: Span },
1635}
1636
1637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1639pub enum ZshGlobQualifierKind {
1640 Classic,
1641 HashQ,
1642}
1643
1644#[derive(Debug, Clone)]
1646pub struct ZshGlobQualifierGroup {
1647 pub span: Span,
1648 pub kind: ZshGlobQualifierKind,
1649 pub fragments: Vec<ZshGlobQualifier>,
1650}
1651
1652#[derive(Debug, Clone)]
1655pub enum ZshGlobQualifier {
1656 Negation {
1657 span: Span,
1658 },
1659 Flag {
1660 name: char,
1661 span: Span,
1662 },
1663 LetterSequence {
1664 text: SourceText,
1665 span: Span,
1666 },
1667 NumericArgument {
1668 span: Span,
1669 start: SourceText,
1670 end: Option<SourceText>,
1671 },
1672}
1673
1674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1676pub enum BraceExpansionKind {
1677 CommaList,
1678 Sequence,
1679}
1680
1681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1683pub enum BraceQuoteContext {
1684 Unquoted,
1685 DoubleQuoted,
1686 SingleQuoted,
1687}
1688
1689#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1691pub enum BraceSyntaxKind {
1692 Expansion(BraceExpansionKind),
1693 Literal,
1694 TemplatePlaceholder,
1695}
1696
1697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1699pub struct BraceSyntax {
1700 pub kind: BraceSyntaxKind,
1701 pub span: Span,
1702 pub quote_context: BraceQuoteContext,
1703}
1704
1705impl BraceSyntax {
1706 pub const fn expansion_kind(self) -> Option<BraceExpansionKind> {
1707 match self.kind {
1708 BraceSyntaxKind::Expansion(kind) => Some(kind),
1709 BraceSyntaxKind::Literal | BraceSyntaxKind::TemplatePlaceholder => None,
1710 }
1711 }
1712
1713 pub const fn is_recognized_expansion(self) -> bool {
1714 matches!(self.kind, BraceSyntaxKind::Expansion(_))
1715 }
1716
1717 pub const fn expands(self) -> bool {
1718 self.is_recognized_expansion() && matches!(self.quote_context, BraceQuoteContext::Unquoted)
1719 }
1720
1721 pub const fn treated_literally(self) -> bool {
1722 !self.expands()
1723 }
1724}
1725
1726#[derive(Debug, Clone)]
1728pub struct WordPartNode {
1729 pub kind: WordPart,
1730 pub span: Span,
1731}
1732
1733impl WordPartNode {
1734 pub fn new(kind: WordPart, span: Span) -> Self {
1735 Self { kind, span }
1736 }
1737}
1738
1739#[derive(Debug, Clone)]
1741pub struct Word {
1742 pub parts: Vec<WordPartNode>,
1743 pub span: Span,
1745 pub brace_syntax: Vec<BraceSyntax>,
1747}
1748
1749impl Word {
1750 pub fn literal(s: impl Into<String>) -> Self {
1752 Self::literal_with_span(s, Span::new())
1753 }
1754
1755 pub fn literal_with_span(s: impl Into<String>, span: Span) -> Self {
1757 Self {
1758 parts: vec![WordPartNode::new(
1759 WordPart::Literal(LiteralText::owned(s.into())),
1760 span,
1761 )],
1762 span,
1763 brace_syntax: Vec::new(),
1764 }
1765 }
1766
1767 pub fn quoted_literal(s: impl Into<String>) -> Self {
1769 Self::quoted_literal_with_span(s, Span::new())
1770 }
1771
1772 pub fn quoted_literal_with_span(s: impl Into<String>, span: Span) -> Self {
1774 Self {
1775 parts: vec![WordPartNode::new(
1776 WordPart::SingleQuoted {
1777 value: SourceText::cooked(span, s.into()),
1778 dollar: false,
1779 },
1780 span,
1781 )],
1782 span,
1783 brace_syntax: Vec::new(),
1784 }
1785 }
1786
1787 pub fn source_literal_with_spans(span: Span, part_span: Span) -> Self {
1789 Self {
1790 parts: vec![WordPartNode::new(
1791 WordPart::Literal(LiteralText::source()),
1792 part_span,
1793 )],
1794 span,
1795 brace_syntax: Vec::new(),
1796 }
1797 }
1798
1799 pub fn quoted_source_literal_with_spans(span: Span, part_span: Span) -> Self {
1801 Self {
1802 parts: vec![WordPartNode::new(
1803 WordPart::SingleQuoted {
1804 value: SourceText::source(part_span),
1805 dollar: false,
1806 },
1807 span,
1808 )],
1809 span,
1810 brace_syntax: Vec::new(),
1811 }
1812 }
1813
1814 pub fn with_span(mut self, span: Span) -> Self {
1816 let previous_span = self.span;
1817 self.span = span;
1818 if let [part] = self.parts.as_mut_slice()
1819 && part.span == previous_span
1820 {
1821 part.span = span;
1822 }
1823 self
1824 }
1825
1826 pub fn part_span(&self, index: usize) -> Option<Span> {
1828 self.parts.get(index).map(|part| part.span)
1829 }
1830
1831 pub fn part(&self, index: usize) -> Option<&WordPart> {
1833 self.parts.get(index).map(|part| &part.kind)
1834 }
1835
1836 pub fn parts_with_spans(&self) -> impl Iterator<Item = (&WordPart, Span)> + '_ {
1838 self.parts.iter().map(|part| (&part.kind, part.span))
1839 }
1840
1841 pub fn brace_syntax(&self) -> &[BraceSyntax] {
1842 &self.brace_syntax
1843 }
1844
1845 pub fn has_active_brace_expansion(&self) -> bool {
1846 self.brace_syntax.iter().copied().any(BraceSyntax::expands)
1847 }
1848
1849 pub fn is_fully_quoted(&self) -> bool {
1850 matches!(self.parts.as_slice(), [part] if part.kind.is_quoted())
1851 }
1852
1853 pub fn quoted_content_span_in_source(&self, source: &str) -> Option<Span> {
1855 if !self.is_fully_quoted() {
1856 return None;
1857 }
1858
1859 let raw = self.span.slice(source);
1860 let quote = raw.chars().next()?;
1861 if !matches!(quote, '"' | '\'') {
1862 return None;
1863 }
1864
1865 let body = raw.strip_prefix(quote)?.strip_suffix(quote)?;
1866 if body.is_empty() {
1867 return None;
1868 }
1869
1870 let start = self.span.start.advanced_by(&raw[..quote.len_utf8()]);
1871 let end = start.advanced_by(body);
1872 Some(Span::from_positions(start, end))
1873 }
1874
1875 pub fn is_fully_double_quoted(&self) -> bool {
1876 matches!(
1877 self.parts.as_slice(),
1878 [WordPartNode {
1879 kind: WordPart::DoubleQuoted { .. },
1880 ..
1881 }]
1882 )
1883 }
1884
1885 pub fn has_quoted_parts(&self) -> bool {
1886 self.parts.iter().any(|part| part.kind.is_quoted())
1887 }
1888
1889 pub fn try_static_text<'a>(&'a self, source: &'a str) -> Option<Cow<'a, str>> {
1892 static_word_text(self, source)
1893 }
1894
1895 pub fn render(&self, source: &str) -> String {
1898 let mut rendered = String::new();
1899 self.render_to_buf(source, &mut rendered);
1900 rendered
1901 }
1902
1903 pub fn render_syntax(&self, source: &str) -> String {
1906 let mut rendered = String::new();
1907 self.render_syntax_to_buf(source, &mut rendered);
1908 rendered
1909 }
1910
1911 pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
1914 assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Decoded));
1915 }
1916
1917 pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
1921 assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Syntax));
1922 }
1923
1924 fn fmt_with_source_mode(
1925 &self,
1926 f: &mut impl fmt::Write,
1927 source: Option<&str>,
1928 mode: RenderMode,
1929 ) -> fmt::Result {
1930 if matches!(mode, RenderMode::Syntax)
1931 && let Some(source) = source
1932 && word_prefers_whole_source_slice_in_syntax(self)
1933 && let Some(slice) = syntax_source_slice(self.span, source)
1934 {
1935 if slice.contains('\n') {
1936 f.write_str(slice)?;
1937 } else {
1938 f.write_str(trim_unescaped_trailing_whitespace(slice))?;
1939 }
1940 return Ok(());
1941 }
1942
1943 for (part, span) in self.parts_with_spans() {
1944 fmt_word_part_with_source_mode(f, part, span, source, mode)?;
1945 }
1946
1947 Ok(())
1948 }
1949}
1950
1951impl fmt::Display for Word {
1952 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1953 self.fmt_with_source_mode(f, None, RenderMode::Decoded)
1954 }
1955}
1956
1957pub fn static_word_text<'a>(word: &'a Word, source: &'a str) -> Option<Cow<'a, str>> {
1960 try_static_word_parts_text(&word.parts, source)
1961}
1962
1963pub fn static_command_name_text<'a>(word: &'a Word, source: &'a str) -> Option<Cow<'a, str>> {
1969 try_static_command_name_parts_text(&word.parts, source, StaticCommandNameContext::Unquoted)
1970}
1971
1972#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1974pub enum StaticCommandWrapperTarget {
1975 NotWrapper,
1977 Wrapper {
1979 target_index: Option<usize>,
1981 },
1982}
1983
1984pub fn static_command_wrapper_target_index<'a>(
1990 word_count: usize,
1991 current_index: usize,
1992 current_name: &str,
1993 mut static_text_at: impl FnMut(usize) -> Option<Cow<'a, str>>,
1994) -> StaticCommandWrapperTarget {
1995 let target_index = match current_name {
1996 "noglob" => next_word_index(word_count, current_index),
1997 "command" => command_wrapper_target_index(word_count, current_index, &mut static_text_at),
1998 "builtin" => builtin_wrapper_target_index(word_count, current_index, &mut static_text_at),
1999 "exec" => exec_wrapper_target_index(word_count, current_index, &mut static_text_at),
2000 _ => return StaticCommandWrapperTarget::NotWrapper,
2001 };
2002
2003 StaticCommandWrapperTarget::Wrapper { target_index }
2004}
2005
2006pub fn try_static_word_parts_text<'a>(
2009 parts: &'a [WordPartNode],
2010 source: &'a str,
2011) -> Option<Cow<'a, str>> {
2012 if let [part] = parts {
2013 return try_static_word_part_text(part, source);
2014 }
2015
2016 let mut result = String::new();
2017 collect_static_word_parts_text(parts, source, &mut result).then_some(Cow::Owned(result))
2018}
2019
2020fn try_static_word_part_text<'a>(part: &'a WordPartNode, source: &'a str) -> Option<Cow<'a, str>> {
2021 match &part.kind {
2022 WordPart::Literal(text) => Some(Cow::Borrowed(text.as_str(source, part.span))),
2023 WordPart::SingleQuoted { value, .. } => Some(Cow::Borrowed(value.slice(source))),
2024 WordPart::DoubleQuoted { parts, .. } => try_static_word_parts_text(parts, source),
2025 _ => None,
2026 }
2027}
2028
2029fn collect_static_word_parts_text(parts: &[WordPartNode], source: &str, out: &mut String) -> bool {
2030 for part in parts {
2031 match &part.kind {
2032 WordPart::Literal(text) => out.push_str(text.as_str(source, part.span)),
2033 WordPart::SingleQuoted { value, .. } => out.push_str(value.slice(source)),
2034 WordPart::DoubleQuoted { parts, .. } => {
2035 if !collect_static_word_parts_text(parts, source, out) {
2036 return false;
2037 }
2038 }
2039 _ => return false,
2040 }
2041 }
2042
2043 true
2044}
2045
2046#[derive(Clone, Copy)]
2047enum StaticCommandNameContext {
2048 Unquoted,
2049 DoubleQuoted,
2050}
2051
2052fn try_static_command_name_parts_text<'a>(
2053 parts: &'a [WordPartNode],
2054 source: &'a str,
2055 context: StaticCommandNameContext,
2056) -> Option<Cow<'a, str>> {
2057 if let [part] = parts {
2058 return try_static_command_name_part_text(part, source, context);
2059 }
2060
2061 let mut result = String::new();
2062 collect_static_command_name_parts_text(parts, source, context, &mut result)
2063 .then_some(Cow::Owned(result))
2064}
2065
2066fn try_static_command_name_part_text<'a>(
2067 part: &'a WordPartNode,
2068 source: &'a str,
2069 context: StaticCommandNameContext,
2070) -> Option<Cow<'a, str>> {
2071 match &part.kind {
2072 WordPart::Literal(text) => Some(decode_static_command_literal(
2073 text.as_str(source, part.span),
2074 context,
2075 )),
2076 WordPart::SingleQuoted { value, .. } => Some(Cow::Borrowed(value.slice(source))),
2077 WordPart::DoubleQuoted { parts, .. } => try_static_command_name_parts_text(
2078 parts,
2079 source,
2080 StaticCommandNameContext::DoubleQuoted,
2081 ),
2082 _ => None,
2083 }
2084}
2085
2086fn collect_static_command_name_parts_text(
2087 parts: &[WordPartNode],
2088 source: &str,
2089 context: StaticCommandNameContext,
2090 out: &mut String,
2091) -> bool {
2092 for part in parts {
2093 match &part.kind {
2094 WordPart::Literal(text) => {
2095 append_static_command_literal(text.as_str(source, part.span), context, out);
2096 }
2097 WordPart::SingleQuoted { value, .. } => out.push_str(value.slice(source)),
2098 WordPart::DoubleQuoted { parts, .. } => {
2099 if !collect_static_command_name_parts_text(
2100 parts,
2101 source,
2102 StaticCommandNameContext::DoubleQuoted,
2103 out,
2104 ) {
2105 return false;
2106 }
2107 }
2108 _ => return false,
2109 }
2110 }
2111
2112 true
2113}
2114
2115fn decode_static_command_literal(text: &str, context: StaticCommandNameContext) -> Cow<'_, str> {
2116 let Some(first_escape) = first_static_command_literal_escape(text.as_bytes()) else {
2117 return Cow::Borrowed(text);
2118 };
2119
2120 let mut result = String::with_capacity(text.len());
2121 append_decoded_static_command_literal(text, first_escape, context, &mut result);
2122 Cow::Owned(result)
2123}
2124
2125fn append_static_command_literal(text: &str, context: StaticCommandNameContext, out: &mut String) {
2126 let Some(first_escape) = first_static_command_literal_escape(text.as_bytes()) else {
2127 out.push_str(text);
2128 return;
2129 };
2130
2131 append_decoded_static_command_literal(text, first_escape, context, out);
2132}
2133
2134fn append_decoded_static_command_literal(
2135 text: &str,
2136 first_escape: usize,
2137 context: StaticCommandNameContext,
2138 out: &mut String,
2139) {
2140 let bytes = text.as_bytes();
2141 let mut copy_start = 0usize;
2142 let mut index = first_escape;
2143
2144 while index < bytes.len() {
2145 if bytes[index] != b'\\' {
2146 index += 1;
2147 continue;
2148 }
2149
2150 out.push_str(&text[copy_start..index]);
2151 index += 1;
2152
2153 let Some(&next) = bytes.get(index) else {
2154 out.push('\\');
2155 return;
2156 };
2157
2158 match context {
2159 StaticCommandNameContext::Unquoted => {
2160 copy_start = if next == b'\n' { index + 1 } else { index };
2161 }
2162 StaticCommandNameContext::DoubleQuoted => match next {
2163 b'$' | b'`' | b'"' | b'\\' => copy_start = index,
2164 b'\n' => copy_start = index + 1,
2165 _ => {
2166 out.push('\\');
2167 copy_start = index;
2168 }
2169 },
2170 }
2171
2172 index += 1;
2173 }
2174
2175 out.push_str(&text[copy_start..]);
2176}
2177
2178fn first_static_command_literal_escape(bytes: &[u8]) -> Option<usize> {
2179 bytes.iter().position(|&byte| byte == b'\\')
2180}
2181
2182fn next_word_index(word_count: usize, current_index: usize) -> Option<usize> {
2183 let index = current_index + 1;
2184 (index < word_count).then_some(index)
2185}
2186
2187fn command_wrapper_target_index<'a>(
2188 word_count: usize,
2189 current_index: usize,
2190 static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
2191) -> Option<usize> {
2192 let mut index = current_index + 1;
2193
2194 while index < word_count {
2195 let Some(arg) = static_text_at(index) else {
2196 return Some(index);
2197 };
2198
2199 if arg == "--" {
2200 return next_word_index(word_count, index);
2201 }
2202
2203 if let Some(options) = arg.strip_prefix('-') {
2204 if options.is_empty() {
2205 return Some(index);
2206 }
2207
2208 let mut lookup_mode = false;
2209 for option in options.chars() {
2210 match option {
2211 'p' => {}
2212 'v' | 'V' => lookup_mode = true,
2213 _ => return None,
2214 }
2215 }
2216
2217 if lookup_mode {
2218 return None;
2219 }
2220
2221 index += 1;
2222 continue;
2223 }
2224
2225 return Some(index);
2226 }
2227
2228 None
2229}
2230
2231fn builtin_wrapper_target_index<'a>(
2232 word_count: usize,
2233 current_index: usize,
2234 static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
2235) -> Option<usize> {
2236 let index = current_index + 1;
2237 if index >= word_count {
2238 return None;
2239 }
2240
2241 let Some(arg) = static_text_at(index) else {
2242 return Some(index);
2243 };
2244
2245 if arg == "--" {
2246 return next_word_index(word_count, index);
2247 }
2248
2249 if arg.starts_with('-') && arg != "-" {
2250 return None;
2251 }
2252
2253 Some(index)
2254}
2255
2256fn exec_wrapper_target_index<'a>(
2257 word_count: usize,
2258 current_index: usize,
2259 static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
2260) -> Option<usize> {
2261 let mut index = current_index + 1;
2262
2263 while index < word_count {
2264 let Some(arg) = static_text_at(index) else {
2265 return Some(index);
2266 };
2267
2268 if arg == "--" {
2269 return next_word_index(word_count, index);
2270 }
2271
2272 if let Some(options) = arg.strip_prefix('-') {
2273 if options.is_empty() {
2274 return Some(index);
2275 }
2276
2277 let mut consumed_words = 1;
2278 for (offset, option) in options.char_indices() {
2279 match option {
2280 'c' | 'l' => {}
2281 'a' => {
2282 let has_attached_name = offset + option.len_utf8() < options.len();
2283 if !has_attached_name {
2284 if index + consumed_words >= word_count {
2285 return None;
2286 }
2287 consumed_words += 1;
2288 }
2289 break;
2290 }
2291 _ => return None,
2292 }
2293 }
2294
2295 index += consumed_words;
2296 continue;
2297 }
2298
2299 return Some(index);
2300 }
2301
2302 None
2303}
2304
2305pub fn is_shell_variable_name(name: &str) -> bool {
2307 let mut chars = name.chars();
2308 match chars.next() {
2309 Some(first) if first == '_' || first.is_ascii_alphabetic() => {
2310 chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
2311 }
2312 _ => false,
2313 }
2314}
2315
2316pub fn word_is_standalone_variable_like(word: &Word) -> bool {
2318 match word.parts.as_slice() {
2319 [part] => matches!(
2320 part.kind,
2321 WordPart::Variable(_)
2322 | WordPart::Parameter(_)
2323 | WordPart::ParameterExpansion { .. }
2324 | WordPart::Length(_)
2325 | WordPart::ArrayAccess(_)
2326 | WordPart::ArrayLength(_)
2327 | WordPart::ArrayIndices(_)
2328 | WordPart::Substring { .. }
2329 | WordPart::ArraySlice { .. }
2330 | WordPart::IndirectExpansion { .. }
2331 | WordPart::PrefixMatch { .. }
2332 | WordPart::Transformation { .. }
2333 ),
2334 _ => false,
2335 }
2336}
2337
2338pub fn word_is_standalone_status_capture(word: &Word) -> bool {
2340 matches!(word.parts.as_slice(), [part] if is_standalone_status_capture_part(&part.kind))
2341}
2342
2343fn is_standalone_status_capture_part(part: &WordPart) -> bool {
2344 match part {
2345 WordPart::Variable(name) => name.as_str() == "?",
2346 WordPart::DoubleQuoted { parts, .. } => {
2347 matches!(parts.as_slice(), [part] if is_standalone_status_capture_part(&part.kind))
2348 }
2349 WordPart::Parameter(parameter) => matches!(
2350 parameter.bourne(),
2351 Some(BourneParameterExpansion::Access { reference })
2352 if reference.name.as_str() == "?" && reference.subscript.is_none()
2353 ),
2354 _ => false,
2355 }
2356}
2357
2358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2360pub enum HeredocBodyMode {
2361 Literal,
2362 Expanding,
2363}
2364
2365impl HeredocBodyMode {
2366 pub const fn expands(self) -> bool {
2367 matches!(self, Self::Expanding)
2368 }
2369}
2370
2371#[derive(Debug, Clone)]
2373pub struct HeredocBodyPartNode {
2374 pub kind: HeredocBodyPart,
2375 pub span: Span,
2376}
2377
2378impl HeredocBodyPartNode {
2379 pub fn new(kind: HeredocBodyPart, span: Span) -> Self {
2380 Self { kind, span }
2381 }
2382}
2383
2384#[derive(Debug, Clone)]
2386pub enum HeredocBodyPart {
2387 Literal(LiteralText),
2388 Variable(Name),
2389 CommandSubstitution {
2390 body: StmtSeq,
2391 syntax: CommandSubstitutionSyntax,
2392 },
2393 ArithmeticExpansion {
2394 expression: SourceText,
2395 expression_ast: Option<ArithmeticExprNode>,
2396 expression_word_ast: Word,
2397 syntax: ArithmeticExpansionSyntax,
2398 },
2399 Parameter(Box<ParameterExpansion>),
2400}
2401
2402#[derive(Debug, Clone)]
2404pub struct HeredocBody {
2405 pub mode: HeredocBodyMode,
2406 pub source_backed: bool,
2407 pub parts: Vec<HeredocBodyPartNode>,
2408 pub span: Span,
2409}
2410
2411impl HeredocBody {
2412 pub fn literal_with_span(s: impl Into<String>, span: Span) -> Self {
2413 Self {
2414 mode: HeredocBodyMode::Literal,
2415 source_backed: true,
2416 parts: vec![HeredocBodyPartNode::new(
2417 HeredocBodyPart::Literal(LiteralText::owned(s.into())),
2418 span,
2419 )],
2420 span,
2421 }
2422 }
2423
2424 pub fn source_literal_with_spans(span: Span, part_span: Span) -> Self {
2425 Self {
2426 mode: HeredocBodyMode::Literal,
2427 source_backed: true,
2428 parts: vec![HeredocBodyPartNode::new(
2429 HeredocBodyPart::Literal(LiteralText::source()),
2430 part_span,
2431 )],
2432 span,
2433 }
2434 }
2435
2436 pub fn with_mode(mut self, mode: HeredocBodyMode) -> Self {
2437 self.mode = mode;
2438 self
2439 }
2440
2441 pub fn with_source_backed(mut self, source_backed: bool) -> Self {
2442 self.source_backed = source_backed;
2443 self
2444 }
2445
2446 pub fn part_span(&self, index: usize) -> Option<Span> {
2447 self.parts.get(index).map(|part| part.span)
2448 }
2449
2450 pub fn part(&self, index: usize) -> Option<&HeredocBodyPart> {
2451 self.parts.get(index).map(|part| &part.kind)
2452 }
2453
2454 pub fn parts_with_spans(&self) -> impl Iterator<Item = (&HeredocBodyPart, Span)> + '_ {
2455 self.parts.iter().map(|part| (&part.kind, part.span))
2456 }
2457
2458 pub fn render(&self, source: &str) -> String {
2459 let mut rendered = String::new();
2460 self.render_to_buf(source, &mut rendered);
2461 rendered
2462 }
2463
2464 pub fn render_syntax(&self, source: &str) -> String {
2465 let mut rendered = String::new();
2466 self.render_syntax_to_buf(source, &mut rendered);
2467 rendered
2468 }
2469
2470 pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
2471 assert_string_write(self.fmt_with_source(rendered, Some(source)));
2472 }
2473
2474 pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
2475 assert_string_write(self.fmt_with_source(rendered, Some(source)));
2476 }
2477
2478 fn fmt_with_source(&self, f: &mut impl fmt::Write, source: Option<&str>) -> fmt::Result {
2479 let source = source.filter(|_| self.source_backed);
2480 if let Some(source) = source
2481 && heredoc_body_prefers_whole_source_slice(self)
2482 && let Some(slice) = syntax_source_slice(self.span, source)
2483 {
2484 f.write_str(slice)?;
2485 return Ok(());
2486 }
2487
2488 for (part, span) in self.parts_with_spans() {
2489 fmt_heredoc_body_part_with_source(f, part, span, source)?;
2490 }
2491
2492 Ok(())
2493 }
2494}
2495
2496impl fmt::Display for HeredocBody {
2497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2498 self.fmt_with_source(f, None)
2499 }
2500}
2501
2502#[derive(Debug, Clone)]
2505pub struct Pattern {
2506 pub parts: Vec<PatternPartNode>,
2507 pub span: Span,
2508}
2509
2510impl Pattern {
2511 pub fn part_span(&self, index: usize) -> Option<Span> {
2513 self.parts.get(index).map(|part| part.span)
2514 }
2515
2516 pub fn is_source_backed(&self) -> bool {
2517 self.parts
2518 .iter()
2519 .all(|part| pattern_part_is_source_backed(&part.kind))
2520 }
2521
2522 pub fn parts_with_spans(&self) -> impl Iterator<Item = (&PatternPart, Span)> + '_ {
2524 self.parts.iter().map(|part| (&part.kind, part.span))
2525 }
2526
2527 pub fn render(&self, source: &str) -> String {
2530 let mut rendered = String::new();
2531 self.render_to_buf(source, &mut rendered);
2532 rendered
2533 }
2534
2535 pub fn render_syntax(&self, source: &str) -> String {
2538 let mut rendered = String::new();
2539 self.render_syntax_to_buf(source, &mut rendered);
2540 rendered
2541 }
2542
2543 pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
2547 assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Decoded));
2548 }
2549
2550 pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
2553 assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Syntax));
2554 }
2555
2556 fn fmt_with_source_mode(
2557 &self,
2558 f: &mut impl fmt::Write,
2559 source: Option<&str>,
2560 mode: RenderMode,
2561 ) -> fmt::Result {
2562 if matches!(mode, RenderMode::Syntax)
2563 && let Some(source) = source
2564 && pattern_prefers_whole_source_slice_in_syntax(self)
2565 && let Some(slice) = syntax_source_slice(self.span, source)
2566 {
2567 if slice.contains('\n') {
2568 f.write_str(slice)?;
2569 } else {
2570 f.write_str(trim_unescaped_trailing_whitespace(slice))?;
2571 }
2572 return Ok(());
2573 }
2574
2575 for (part, span) in self.parts_with_spans() {
2576 fmt_pattern_part_with_source_mode(f, part, span, source, mode)?;
2577 }
2578
2579 Ok(())
2580 }
2581}
2582
2583impl fmt::Display for Pattern {
2584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2585 self.fmt_with_source_mode(f, None, RenderMode::Decoded)
2586 }
2587}
2588
2589#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2591pub enum PatternGroupKind {
2592 ZeroOrOne,
2593 ZeroOrMore,
2594 OneOrMore,
2595 ExactlyOne,
2596 NoneOf,
2597}
2598
2599impl PatternGroupKind {
2600 pub fn prefix(self) -> char {
2601 match self {
2602 Self::ZeroOrOne => '?',
2603 Self::ZeroOrMore => '*',
2604 Self::OneOrMore => '+',
2605 Self::ExactlyOne => '@',
2606 Self::NoneOf => '!',
2607 }
2608 }
2609}
2610
2611#[derive(Debug, Clone)]
2613pub struct PatternPartNode {
2614 pub kind: PatternPart,
2615 pub span: Span,
2616}
2617
2618impl PatternPartNode {
2619 pub fn new(kind: PatternPart, span: Span) -> Self {
2620 Self { kind, span }
2621 }
2622}
2623
2624#[derive(Debug, Clone)]
2626pub enum PatternPart {
2627 Literal(LiteralText),
2628 AnyString,
2629 AnyChar,
2630 CharClass(SourceText),
2631 Group {
2632 kind: PatternGroupKind,
2633 patterns: Vec<Pattern>,
2634 },
2635 Word(Word),
2636}
2637
2638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2639enum RenderMode {
2640 Decoded,
2641 Syntax,
2642}
2643
2644fn syntax_source_slice(span: Span, source: &str) -> Option<&str> {
2645 (span.start.offset < span.end.offset && span.end.offset <= source.len())
2646 .then(|| span.slice(source))
2647}
2648
2649fn word_prefers_whole_source_slice_in_syntax(word: &Word) -> bool {
2650 matches!(
2651 word.parts.as_slice(),
2652 [part] if part.span == word.span && top_level_word_part_prefers_source_slice_in_syntax(&part.kind)
2653 )
2654}
2655
2656fn top_level_word_part_prefers_source_slice_in_syntax(part: &WordPart) -> bool {
2657 match part {
2658 WordPart::Literal(text) => text.is_source_backed(),
2659 WordPart::SingleQuoted { value, .. } => value.is_source_backed(),
2660 WordPart::DoubleQuoted { parts, .. } => parts.iter().all(|part| match &part.kind {
2661 WordPart::Literal(_) => true,
2662 other => part_prefers_source_slice_in_syntax(other) && part_is_source_backed(other),
2663 }),
2664 _ => part_prefers_source_slice_in_syntax(part) && part_is_source_backed(part),
2665 }
2666}
2667
2668fn pattern_prefers_whole_source_slice_in_syntax(pattern: &Pattern) -> bool {
2669 !pattern.parts.is_empty()
2670 && pattern
2671 .parts
2672 .iter()
2673 .all(|part| top_level_pattern_part_prefers_source_slice_in_syntax(&part.kind))
2674}
2675
2676fn heredoc_body_prefers_whole_source_slice(body: &HeredocBody) -> bool {
2677 !body.parts.is_empty()
2678 && body
2679 .parts
2680 .iter()
2681 .all(|part| heredoc_body_part_is_source_backed(&part.kind))
2682}
2683
2684fn top_level_pattern_part_prefers_source_slice_in_syntax(part: &PatternPart) -> bool {
2685 match part {
2686 PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => true,
2687 PatternPart::CharClass(text) => text.is_source_backed(),
2688 PatternPart::Group { patterns, .. } => patterns
2689 .iter()
2690 .all(pattern_prefers_whole_source_slice_in_syntax),
2691 PatternPart::Word(word) => word_prefers_whole_source_slice_in_syntax(word),
2692 }
2693}
2694
2695fn display_source_text<'a>(text: Option<&'a SourceText>, source: Option<&'a str>) -> &'a str {
2696 match (text, source) {
2697 (Some(text), Some(source)) => text.slice(source),
2698 (
2699 Some(SourceText {
2700 cooked: Some(text), ..
2701 }),
2702 None,
2703 ) => text.as_ref(),
2704 (Some(_), None) => "...",
2705 (None, _) => "",
2706 }
2707}
2708
2709fn display_subscript_text<'a>(subscript: &'a Subscript, source: Option<&'a str>) -> Cow<'a, str> {
2710 match (source, subscript.selector()) {
2711 (Some(source), _) => Cow::Borrowed(subscript.syntax_text(source)),
2712 (None, Some(selector)) => Cow::Owned(selector.as_char().to_string()),
2713 (None, None) => Cow::Borrowed(display_source_text(
2714 Some(subscript.syntax_source_text()),
2715 source,
2716 )),
2717 }
2718}
2719
2720fn fmt_var_ref_with_source(
2721 f: &mut impl fmt::Write,
2722 reference: &VarRef,
2723 source: Option<&str>,
2724) -> fmt::Result {
2725 write!(f, "{}", reference.name)?;
2726 if let Some(subscript) = &reference.subscript {
2727 write!(f, "[{}]", display_subscript_text(subscript, source))?;
2728 }
2729 Ok(())
2730}
2731
2732#[derive(Debug, Clone)]
2734pub enum WordPart {
2735 Literal(LiteralText),
2737 ZshQualifiedGlob(ZshQualifiedGlob),
2739 SingleQuoted { value: SourceText, dollar: bool },
2741 DoubleQuoted {
2743 parts: Vec<WordPartNode>,
2744 dollar: bool,
2745 },
2746 Variable(Name),
2748 CommandSubstitution {
2750 body: StmtSeq,
2751 syntax: CommandSubstitutionSyntax,
2752 },
2753 ArithmeticExpansion {
2755 expression: SourceText,
2756 expression_ast: Option<ArithmeticExprNode>,
2758 expression_word_ast: Word,
2760 syntax: ArithmeticExpansionSyntax,
2761 },
2762 Parameter(ParameterExpansion),
2764 ParameterExpansion {
2767 reference: VarRef,
2768 operator: ParameterOp,
2769 operand: Option<SourceText>,
2770 operand_word_ast: Option<Word>,
2771 colon_variant: bool,
2772 },
2773 Length(VarRef),
2775 ArrayAccess(VarRef),
2777 ArrayLength(VarRef),
2779 ArrayIndices(VarRef),
2781 Substring {
2783 reference: VarRef,
2784 offset: SourceText,
2785 offset_ast: Option<ArithmeticExprNode>,
2787 offset_word_ast: Word,
2789 length: Option<SourceText>,
2790 length_ast: Option<ArithmeticExprNode>,
2792 length_word_ast: Option<Word>,
2794 },
2795 ArraySlice {
2797 reference: VarRef,
2798 offset: SourceText,
2799 offset_ast: Option<ArithmeticExprNode>,
2801 offset_word_ast: Word,
2803 length: Option<SourceText>,
2804 length_ast: Option<ArithmeticExprNode>,
2806 length_word_ast: Option<Word>,
2808 },
2809 IndirectExpansion {
2812 reference: VarRef,
2813 operator: Option<ParameterOp>,
2814 operand: Option<SourceText>,
2815 operand_word_ast: Option<Word>,
2816 colon_variant: bool,
2817 },
2818 PrefixMatch { prefix: Name, kind: PrefixMatchKind },
2820 ProcessSubstitution {
2822 body: StmtSeq,
2824 is_input: bool,
2826 },
2827 Transformation { reference: VarRef, operator: char },
2829}
2830
2831impl WordPart {
2832 pub fn is_quoted(&self) -> bool {
2833 matches!(self, Self::SingleQuoted { .. } | Self::DoubleQuoted { .. })
2834 }
2835}
2836
2837#[derive(Debug, Clone)]
2839pub struct ArrayExpr {
2840 pub kind: ArrayKind,
2841 pub elements: Vec<ArrayElem>,
2842 pub span: Span,
2843}
2844
2845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2847pub enum ArrayKind {
2848 Indexed,
2849 Associative,
2850 Contextual,
2851}
2852
2853#[derive(Debug, Clone)]
2855pub struct ArrayValueWord {
2856 pub word: Word,
2857 pub has_top_level_unquoted_comma: bool,
2858}
2859
2860impl ArrayValueWord {
2861 pub fn new(word: Word, has_top_level_unquoted_comma: bool) -> Self {
2862 Self {
2863 word,
2864 has_top_level_unquoted_comma,
2865 }
2866 }
2867
2868 pub fn has_top_level_unquoted_comma(&self) -> bool {
2869 self.has_top_level_unquoted_comma
2870 }
2871
2872 pub fn span(&self) -> Span {
2873 self.word.span
2874 }
2875}
2876
2877impl From<Word> for ArrayValueWord {
2878 fn from(word: Word) -> Self {
2879 Self::new(word, false)
2880 }
2881}
2882
2883impl Deref for ArrayValueWord {
2884 type Target = Word;
2885
2886 fn deref(&self) -> &Self::Target {
2887 &self.word
2888 }
2889}
2890
2891impl DerefMut for ArrayValueWord {
2892 fn deref_mut(&mut self) -> &mut Self::Target {
2893 &mut self.word
2894 }
2895}
2896
2897#[derive(Debug, Clone)]
2899pub enum ArrayElem {
2900 Sequential(ArrayValueWord),
2901 Keyed {
2902 key: Subscript,
2903 value: ArrayValueWord,
2904 },
2905 KeyedAppend {
2906 key: Subscript,
2907 value: ArrayValueWord,
2908 },
2909}
2910
2911impl ArrayElem {
2912 pub fn span(&self) -> Span {
2913 match self {
2914 Self::Sequential(word) => word.span(),
2915 Self::Keyed { key, value } | Self::KeyedAppend { key, value } => {
2916 key.span().merge(value.span())
2917 }
2918 }
2919 }
2920
2921 pub fn value(&self) -> &ArrayValueWord {
2922 match self {
2923 Self::Sequential(word) => word,
2924 Self::Keyed { value, .. } | Self::KeyedAppend { value, .. } => value,
2925 }
2926 }
2927
2928 pub fn value_mut(&mut self) -> &mut ArrayValueWord {
2929 match self {
2930 Self::Sequential(word) => word,
2931 Self::Keyed { value, .. } | Self::KeyedAppend { value, .. } => value,
2932 }
2933 }
2934}
2935
2936fn fmt_literal_text(
2937 f: &mut impl fmt::Write,
2938 text: &LiteralText,
2939 span: Span,
2940 source: Option<&str>,
2941) -> fmt::Result {
2942 match source {
2943 Some(source) => f.write_str(text.as_str(source, span)),
2944 None => match text {
2945 LiteralText::Source => f.write_str("<source>"),
2946 LiteralText::Owned(text) | LiteralText::CookedSource(text) => f.write_str(text),
2947 },
2948 }
2949}
2950
2951fn fmt_double_quoted_literal_text(
2952 f: &mut impl fmt::Write,
2953 text: &LiteralText,
2954 span: Span,
2955 source: Option<&str>,
2956) -> fmt::Result {
2957 let rendered = match source {
2958 Some(source) => text.as_str(source, span),
2959 None => match text {
2960 LiteralText::Source => "<source>",
2961 LiteralText::Owned(text) | LiteralText::CookedSource(text) => text,
2962 },
2963 };
2964
2965 for ch in rendered.chars() {
2966 match ch {
2967 '"' | '\\' | '$' | '`' => {
2968 f.write_char('\\')?;
2969 f.write_char(ch)?;
2970 }
2971 _ => f.write_char(ch)?,
2972 }
2973 }
2974
2975 Ok(())
2976}
2977
2978fn fmt_pattern_part_with_source_mode(
2979 f: &mut impl fmt::Write,
2980 part: &PatternPart,
2981 span: Span,
2982 source: Option<&str>,
2983 mode: RenderMode,
2984) -> fmt::Result {
2985 match part {
2986 PatternPart::Literal(text) => match (mode, source) {
2987 (RenderMode::Syntax, Some(source))
2988 if text.is_source_backed() && span.end.offset <= source.len() =>
2989 {
2990 f.write_str(text.syntax_str(source, span))?
2991 }
2992 _ => fmt_literal_text(f, text, span, source)?,
2993 },
2994 PatternPart::AnyString => f.write_str("*")?,
2995 PatternPart::AnyChar => f.write_str("?")?,
2996 PatternPart::CharClass(text) => match source {
2997 Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
2998 _ => f.write_str(display_source_text(Some(text), source))?,
2999 },
3000 PatternPart::Group { kind, patterns } => {
3001 write!(f, "{}(", kind.prefix())?;
3002 let mut patterns = patterns.iter();
3003 if let Some(pattern) = patterns.next() {
3004 pattern.fmt_with_source_mode(f, source, mode)?;
3005 for pattern in patterns {
3006 f.write_str("|")?;
3007 pattern.fmt_with_source_mode(f, source, mode)?;
3008 }
3009 }
3010 f.write_str(")")?;
3011 }
3012 PatternPart::Word(word) => word.fmt_with_source_mode(f, source, mode)?,
3013 }
3014
3015 Ok(())
3016}
3017
3018fn fmt_word_part_with_source_mode(
3019 f: &mut impl fmt::Write,
3020 part: &WordPart,
3021 span: Span,
3022 source: Option<&str>,
3023 mode: RenderMode,
3024) -> fmt::Result {
3025 if matches!(mode, RenderMode::Syntax)
3026 && let Some(source) = source
3027 && part_prefers_source_slice_in_syntax(part)
3028 && part_is_source_backed(part)
3029 && span.end.offset <= source.len()
3030 {
3031 f.write_str(span.slice(source))?;
3032 return Ok(());
3033 }
3034
3035 match part {
3036 WordPart::Literal(text) => match (mode, source) {
3037 (RenderMode::Syntax, Some(source))
3038 if text.is_source_backed() && span.end.offset <= source.len() =>
3039 {
3040 f.write_str(trim_unescaped_trailing_whitespace(span.slice(source)))?;
3041 }
3042 _ => fmt_literal_text(f, text, span, source)?,
3043 },
3044 WordPart::ZshQualifiedGlob(glob) => {
3045 if let Some(source) = source
3046 && zsh_qualified_glob_is_source_backed(glob)
3047 && glob.span.end.offset <= source.len()
3048 {
3049 f.write_str(trim_unescaped_trailing_whitespace(glob.span.slice(source)))?;
3050 } else {
3051 for segment in &glob.segments {
3052 fmt_zsh_glob_segment_with_source(f, segment, source)?;
3053 }
3054 if let Some(qualifiers) = &glob.qualifiers {
3055 fmt_zsh_glob_qualifier_group_with_source(f, qualifiers, source)?;
3056 }
3057 }
3058 }
3059 WordPart::SingleQuoted { value, dollar } => match mode {
3060 RenderMode::Decoded => f.write_str(display_source_text(Some(value), source))?,
3061 RenderMode::Syntax => match source {
3062 Some(source)
3063 if value.is_source_backed()
3064 && part_is_source_backed(part)
3065 && span.end.offset <= source.len() =>
3066 {
3067 f.write_str(span.slice(source))?;
3068 }
3069 _ => {
3070 if *dollar {
3071 f.write_str("$")?;
3072 }
3073 f.write_str("'")?;
3074 f.write_str(display_source_text(Some(value), source))?;
3075 f.write_str("'")?;
3076 }
3077 },
3078 },
3079 WordPart::DoubleQuoted { parts, dollar } => match mode {
3080 RenderMode::Decoded => {
3081 for part in parts {
3082 fmt_word_part_with_source_mode(f, &part.kind, part.span, source, mode)?;
3083 }
3084 }
3085 RenderMode::Syntax => match source {
3086 Some(source) if part_is_source_backed(part) && span.end.offset <= source.len() => {
3087 f.write_str(span.slice(source))?;
3088 }
3089 _ => {
3090 if *dollar {
3091 f.write_str("$")?;
3092 }
3093 f.write_str("\"")?;
3094 for part in parts {
3095 match &part.kind {
3096 WordPart::Literal(text) => {
3099 fmt_double_quoted_literal_text(f, text, part.span, source)?;
3100 }
3101 _ => {
3102 fmt_word_part_with_source_mode(
3103 f, &part.kind, part.span, source, mode,
3104 )?;
3105 }
3106 }
3107 }
3108 f.write_str("\"")?;
3109 }
3110 },
3111 },
3112 WordPart::Variable(name) => write!(f, "${}", name)?,
3113 WordPart::CommandSubstitution { body, syntax } => match source {
3114 Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
3115 _ => match syntax {
3116 CommandSubstitutionSyntax::DollarParen => write!(f, "$({:?})", body)?,
3117 CommandSubstitutionSyntax::Backtick => write!(f, "`{:?}`", body)?,
3118 },
3119 },
3120 WordPart::ArithmeticExpansion {
3121 expression, syntax, ..
3122 } => match source {
3123 Some(source) if expression.is_source_backed() && span.end.offset <= source.len() => {
3124 f.write_str(span.slice(source))?
3125 }
3126 _ => match syntax {
3127 ArithmeticExpansionSyntax::DollarParenParen => {
3128 write!(f, "$(({}))", display_source_text(Some(expression), source))?
3129 }
3130 ArithmeticExpansionSyntax::LegacyBracket => {
3131 write!(f, "$[{}]", display_source_text(Some(expression), source))?
3132 }
3133 },
3134 },
3135 WordPart::Parameter(parameter) => {
3136 write!(
3137 f,
3138 "${{{}}}",
3139 display_source_text(Some(¶meter.raw_body), source)
3140 )?;
3141 }
3142 WordPart::ParameterExpansion {
3143 reference,
3144 operator,
3145 operand,
3146 colon_variant,
3147 ..
3148 } => match operator {
3149 ParameterOp::UseDefault => {
3150 let c = if *colon_variant { ":" } else { "" };
3151 write!(f, "${{")?;
3152 fmt_var_ref_with_source(f, reference, source)?;
3153 write!(
3154 f,
3155 "{}-{}}}",
3156 c,
3157 display_source_text(operand.as_ref(), source)
3158 )?
3159 }
3160 ParameterOp::AssignDefault => {
3161 let c = if *colon_variant { ":" } else { "" };
3162 write!(f, "${{")?;
3163 fmt_var_ref_with_source(f, reference, source)?;
3164 write!(
3165 f,
3166 "{}={}}}",
3167 c,
3168 display_source_text(operand.as_ref(), source)
3169 )?
3170 }
3171 ParameterOp::UseReplacement => {
3172 let c = if *colon_variant { ":" } else { "" };
3173 write!(f, "${{")?;
3174 fmt_var_ref_with_source(f, reference, source)?;
3175 write!(
3176 f,
3177 "{}+{}}}",
3178 c,
3179 display_source_text(operand.as_ref(), source)
3180 )?
3181 }
3182 ParameterOp::Error => {
3183 let c = if *colon_variant { ":" } else { "" };
3184 write!(f, "${{")?;
3185 fmt_var_ref_with_source(f, reference, source)?;
3186 write!(
3187 f,
3188 "{}?{}}}",
3189 c,
3190 display_source_text(operand.as_ref(), source)
3191 )?
3192 }
3193 ParameterOp::RemovePrefixShort { pattern } => {
3194 write!(f, "${{")?;
3195 fmt_var_ref_with_source(f, reference, source)?;
3196 f.write_str("#")?;
3197 pattern.fmt_with_source_mode(f, source, mode)?;
3198 f.write_str("}")?;
3199 }
3200 ParameterOp::RemovePrefixLong { pattern } => {
3201 write!(f, "${{")?;
3202 fmt_var_ref_with_source(f, reference, source)?;
3203 f.write_str("##")?;
3204 pattern.fmt_with_source_mode(f, source, mode)?;
3205 f.write_str("}")?;
3206 }
3207 ParameterOp::RemoveSuffixShort { pattern } => {
3208 write!(f, "${{")?;
3209 fmt_var_ref_with_source(f, reference, source)?;
3210 f.write_str("%")?;
3211 pattern.fmt_with_source_mode(f, source, mode)?;
3212 f.write_str("}")?;
3213 }
3214 ParameterOp::RemoveSuffixLong { pattern } => {
3215 write!(f, "${{")?;
3216 fmt_var_ref_with_source(f, reference, source)?;
3217 f.write_str("%%")?;
3218 pattern.fmt_with_source_mode(f, source, mode)?;
3219 f.write_str("}")?;
3220 }
3221 ParameterOp::ReplaceFirst {
3222 pattern,
3223 replacement,
3224 ..
3225 } => {
3226 write!(f, "${{")?;
3227 fmt_var_ref_with_source(f, reference, source)?;
3228 f.write_str("/")?;
3229 pattern.fmt_with_source_mode(f, source, mode)?;
3230 write!(f, "/{}}}", display_source_text(Some(replacement), source))?;
3231 }
3232 ParameterOp::ReplaceAll {
3233 pattern,
3234 replacement,
3235 ..
3236 } => {
3237 write!(f, "${{")?;
3238 fmt_var_ref_with_source(f, reference, source)?;
3239 f.write_str("//")?;
3240 pattern.fmt_with_source_mode(f, source, mode)?;
3241 write!(f, "/{}}}", display_source_text(Some(replacement), source))?;
3242 }
3243 ParameterOp::UpperFirst => {
3244 write!(f, "${{")?;
3245 fmt_var_ref_with_source(f, reference, source)?;
3246 f.write_str("^}")?;
3247 }
3248 ParameterOp::UpperAll => {
3249 write!(f, "${{")?;
3250 fmt_var_ref_with_source(f, reference, source)?;
3251 f.write_str("^^}")?;
3252 }
3253 ParameterOp::LowerFirst => {
3254 write!(f, "${{")?;
3255 fmt_var_ref_with_source(f, reference, source)?;
3256 f.write_str(",}")?;
3257 }
3258 ParameterOp::LowerAll => {
3259 write!(f, "${{")?;
3260 fmt_var_ref_with_source(f, reference, source)?;
3261 f.write_str(",,}")?;
3262 }
3263 },
3264 WordPart::Length(reference) => {
3265 write!(f, "${{#")?;
3266 fmt_var_ref_with_source(f, reference, source)?;
3267 f.write_str("}")?;
3268 }
3269 WordPart::ArrayAccess(reference) => {
3270 write!(f, "${{")?;
3271 fmt_var_ref_with_source(f, reference, source)?;
3272 f.write_str("}")?;
3273 }
3274 WordPart::ArrayLength(reference) => {
3275 write!(f, "${{#")?;
3276 fmt_var_ref_with_source(f, reference, source)?;
3277 f.write_str("}")?;
3278 }
3279 WordPart::ArrayIndices(reference) => {
3280 write!(f, "${{!")?;
3281 fmt_var_ref_with_source(f, reference, source)?;
3282 f.write_str("}")?;
3283 }
3284 WordPart::Substring {
3285 reference,
3286 offset,
3287 length,
3288 ..
3289 } => {
3290 if let Some(length) = length {
3291 write!(f, "${{")?;
3292 fmt_var_ref_with_source(f, reference, source)?;
3293 write!(
3294 f,
3295 ":{}:{}}}",
3296 display_source_text(Some(offset), source),
3297 display_source_text(Some(length), source)
3298 )?
3299 } else {
3300 write!(f, "${{")?;
3301 fmt_var_ref_with_source(f, reference, source)?;
3302 write!(f, ":{}}}", display_source_text(Some(offset), source))?
3303 }
3304 }
3305 WordPart::ArraySlice {
3306 reference,
3307 offset,
3308 length,
3309 ..
3310 } => {
3311 if let Some(length) = length {
3312 write!(f, "${{")?;
3313 fmt_var_ref_with_source(f, reference, source)?;
3314 write!(
3315 f,
3316 ":{}:{}}}",
3317 display_source_text(Some(offset), source),
3318 display_source_text(Some(length), source)
3319 )?
3320 } else {
3321 write!(f, "${{")?;
3322 fmt_var_ref_with_source(f, reference, source)?;
3323 write!(f, ":{}}}", display_source_text(Some(offset), source))?
3324 }
3325 }
3326 WordPart::IndirectExpansion {
3327 reference,
3328 operator,
3329 operand,
3330 colon_variant,
3331 ..
3332 } => {
3333 let mut reference_syntax = String::new();
3334 fmt_var_ref_with_source(&mut reference_syntax, reference, source)?;
3335 if let Some(op) = operator {
3336 let c = if *colon_variant { ":" } else { "" };
3337 let op_char = match op {
3338 ParameterOp::UseDefault => "-",
3339 ParameterOp::AssignDefault => "=",
3340 ParameterOp::UseReplacement => "+",
3341 ParameterOp::Error => "?",
3342 _ => "",
3343 };
3344 write!(
3345 f,
3346 "${{!{}{}{}{}}}",
3347 reference_syntax,
3348 c,
3349 op_char,
3350 display_source_text(operand.as_ref(), source)
3351 )?
3352 } else {
3353 write!(f, "${{!{}}}", reference_syntax)?
3354 }
3355 }
3356 WordPart::PrefixMatch { prefix, kind } => write!(f, "${{!{}{}}}", prefix, kind.as_char())?,
3357 WordPart::ProcessSubstitution { body, is_input } => match source {
3358 Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
3359 _ => {
3360 let prefix = if *is_input { "<" } else { ">" };
3361 write!(f, "{}({:?})", prefix, body)?
3362 }
3363 },
3364 WordPart::Transformation {
3365 reference,
3366 operator,
3367 } => {
3368 write!(f, "${{")?;
3369 fmt_var_ref_with_source(f, reference, source)?;
3370 write!(f, "@{}}}", operator)?;
3371 }
3372 }
3373
3374 Ok(())
3375}
3376
3377fn fmt_heredoc_body_part_with_source(
3378 f: &mut impl fmt::Write,
3379 part: &HeredocBodyPart,
3380 span: Span,
3381 source: Option<&str>,
3382) -> fmt::Result {
3383 if let Some(source) = source
3384 && heredoc_body_part_is_source_backed(part)
3385 && span.end.offset <= source.len()
3386 {
3387 f.write_str(span.slice(source))?;
3388 return Ok(());
3389 }
3390
3391 match part {
3392 HeredocBodyPart::Literal(text) => fmt_literal_text(f, text, span, source)?,
3393 HeredocBodyPart::Variable(name) => write!(f, "${}", name)?,
3394 HeredocBodyPart::CommandSubstitution { body, syntax } => match syntax {
3395 CommandSubstitutionSyntax::DollarParen => write!(f, "$({:?})", body)?,
3396 CommandSubstitutionSyntax::Backtick => write!(f, "`{:?}`", body)?,
3397 },
3398 HeredocBodyPart::ArithmeticExpansion {
3399 expression, syntax, ..
3400 } => match syntax {
3401 ArithmeticExpansionSyntax::DollarParenParen => {
3402 write!(f, "$(({}))", display_source_text(Some(expression), source))?
3403 }
3404 ArithmeticExpansionSyntax::LegacyBracket => {
3405 write!(f, "$[{}]", display_source_text(Some(expression), source))?
3406 }
3407 },
3408 HeredocBodyPart::Parameter(parameter) => {
3409 write!(
3410 f,
3411 "${{{}}}",
3412 display_source_text(Some(¶meter.raw_body), source)
3413 )?;
3414 }
3415 }
3416
3417 Ok(())
3418}
3419
3420fn part_prefers_source_slice_in_syntax(part: &WordPart) -> bool {
3421 matches!(
3422 part,
3423 WordPart::Variable(_)
3424 | WordPart::ZshQualifiedGlob(_)
3425 | WordPart::CommandSubstitution { .. }
3426 | WordPart::ArithmeticExpansion { .. }
3427 | WordPart::Parameter(_)
3428 | WordPart::ParameterExpansion { .. }
3429 | WordPart::Length(_)
3430 | WordPart::ArrayAccess(_)
3431 | WordPart::ArrayLength(_)
3432 | WordPart::ArrayIndices(_)
3433 | WordPart::Substring { .. }
3434 | WordPart::ArraySlice { .. }
3435 | WordPart::IndirectExpansion { .. }
3436 | WordPart::PrefixMatch { .. }
3437 | WordPart::ProcessSubstitution { .. }
3438 | WordPart::Transformation { .. }
3439 )
3440}
3441
3442fn trim_unescaped_trailing_whitespace(text: &str) -> &str {
3443 let mut end = text.len();
3444 while end > 0 {
3445 let Some((whitespace_start, ch)) = text[..end].char_indices().next_back() else {
3446 break;
3447 };
3448 if !ch.is_whitespace() {
3449 break;
3450 }
3451
3452 let backslash_count = text.as_bytes()[..whitespace_start]
3453 .iter()
3454 .rev()
3455 .take_while(|byte| **byte == b'\\')
3456 .count();
3457 if backslash_count % 2 == 1 {
3458 break;
3459 }
3460
3461 end = whitespace_start;
3462 }
3463
3464 &text[..end]
3465}
3466
3467fn part_is_source_backed(part: &WordPart) -> bool {
3468 match part {
3469 WordPart::Literal(text) => text.is_source_backed(),
3470 WordPart::ZshQualifiedGlob(glob) => zsh_qualified_glob_is_source_backed(glob),
3471 WordPart::SingleQuoted { value, .. } => value.is_source_backed(),
3472 WordPart::DoubleQuoted { parts, .. } => {
3473 parts.iter().all(|part| part_is_source_backed(&part.kind))
3474 }
3475 WordPart::Parameter(parameter) => parameter.raw_body.is_source_backed(),
3476 WordPart::ArithmeticExpansion { expression, .. } => expression.is_source_backed(),
3477 WordPart::ParameterExpansion {
3478 reference,
3479 operand,
3480 operator,
3481 ..
3482 } => {
3483 reference.is_source_backed()
3484 && operator_is_source_backed(operator)
3485 && operand.as_ref().is_none_or(SourceText::is_source_backed)
3486 }
3487 WordPart::Length(reference)
3488 | WordPart::ArrayAccess(reference)
3489 | WordPart::ArrayLength(reference)
3490 | WordPart::ArrayIndices(reference)
3491 | WordPart::Transformation { reference, .. } => reference.is_source_backed(),
3492 WordPart::Substring {
3493 reference,
3494 offset: index,
3495 ..
3496 }
3497 | WordPart::ArraySlice {
3498 reference,
3499 offset: index,
3500 ..
3501 } => reference.is_source_backed() && index.is_source_backed(),
3502 WordPart::IndirectExpansion {
3503 reference,
3504 operand,
3505 operator,
3506 ..
3507 } => {
3508 reference.is_source_backed()
3509 && operator.is_none()
3510 && operand.as_ref().is_none_or(SourceText::is_source_backed)
3511 }
3512 WordPart::CommandSubstitution { .. }
3513 | WordPart::Variable(_)
3514 | WordPart::PrefixMatch { .. }
3515 | WordPart::ProcessSubstitution { .. } => true,
3516 }
3517}
3518
3519fn heredoc_body_part_is_source_backed(part: &HeredocBodyPart) -> bool {
3520 match part {
3521 HeredocBodyPart::Literal(text) => text.is_source_backed(),
3522 HeredocBodyPart::Variable(_) | HeredocBodyPart::CommandSubstitution { .. } => true,
3523 HeredocBodyPart::ArithmeticExpansion { expression, .. } => expression.is_source_backed(),
3524 HeredocBodyPart::Parameter(parameter) => parameter.raw_body.is_source_backed(),
3525 }
3526}
3527
3528fn pattern_part_is_source_backed(part: &PatternPart) -> bool {
3529 match part {
3530 PatternPart::Literal(text) => text.is_source_backed(),
3531 PatternPart::AnyString | PatternPart::AnyChar => true,
3532 PatternPart::CharClass(text) => text.is_source_backed(),
3533 PatternPart::Group { patterns, .. } => patterns.iter().all(Pattern::is_source_backed),
3534 PatternPart::Word(word) => word
3535 .parts
3536 .iter()
3537 .all(|part| part_is_source_backed(&part.kind)),
3538 }
3539}
3540
3541fn zsh_qualified_glob_is_source_backed(glob: &ZshQualifiedGlob) -> bool {
3542 glob.segments.iter().all(zsh_glob_segment_is_source_backed)
3543 && glob
3544 .qualifiers
3545 .as_ref()
3546 .is_none_or(zsh_glob_qualifier_group_is_source_backed)
3547}
3548
3549fn zsh_glob_segment_is_source_backed(segment: &ZshGlobSegment) -> bool {
3550 match segment {
3551 ZshGlobSegment::Pattern(pattern) => pattern.is_source_backed(),
3552 ZshGlobSegment::InlineControl(control) => zsh_inline_glob_control_is_source_backed(control),
3553 }
3554}
3555
3556fn zsh_inline_glob_control_is_source_backed(_control: &ZshInlineGlobControl) -> bool {
3557 true
3558}
3559
3560fn fmt_zsh_glob_segment_with_source(
3561 f: &mut impl fmt::Write,
3562 segment: &ZshGlobSegment,
3563 source: Option<&str>,
3564) -> fmt::Result {
3565 match segment {
3566 ZshGlobSegment::Pattern(pattern) => {
3567 pattern.fmt_with_source_mode(f, source, RenderMode::Syntax)
3568 }
3569 ZshGlobSegment::InlineControl(control) => {
3570 fmt_zsh_inline_glob_control_with_source(f, control, source)
3571 }
3572 }
3573}
3574
3575fn fmt_zsh_inline_glob_control_with_source(
3576 f: &mut impl fmt::Write,
3577 control: &ZshInlineGlobControl,
3578 _source: Option<&str>,
3579) -> fmt::Result {
3580 match control {
3581 ZshInlineGlobControl::CaseInsensitive { .. } => f.write_str("(#i)"),
3582 ZshInlineGlobControl::Backreferences { .. } => f.write_str("(#b)"),
3583 ZshInlineGlobControl::StartAnchor { .. } => f.write_str("(#s)"),
3584 ZshInlineGlobControl::EndAnchor { .. } => f.write_str("(#e)"),
3585 }
3586}
3587
3588fn zsh_glob_qualifier_group_is_source_backed(group: &ZshGlobQualifierGroup) -> bool {
3589 group
3590 .fragments
3591 .iter()
3592 .all(zsh_glob_qualifier_is_source_backed)
3593}
3594
3595fn zsh_glob_qualifier_is_source_backed(fragment: &ZshGlobQualifier) -> bool {
3596 match fragment {
3597 ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => true,
3598 ZshGlobQualifier::LetterSequence { text, .. } => text.is_source_backed(),
3599 ZshGlobQualifier::NumericArgument { start, end, .. } => {
3600 start.is_source_backed() && end.as_ref().is_none_or(SourceText::is_source_backed)
3601 }
3602 }
3603}
3604
3605fn fmt_zsh_glob_qualifier_group_with_source(
3606 f: &mut impl fmt::Write,
3607 group: &ZshGlobQualifierGroup,
3608 source: Option<&str>,
3609) -> fmt::Result {
3610 match group.kind {
3611 ZshGlobQualifierKind::Classic => f.write_str("(")?,
3612 ZshGlobQualifierKind::HashQ => f.write_str("(#q")?,
3613 }
3614 for fragment in &group.fragments {
3615 match fragment {
3616 ZshGlobQualifier::Negation { .. } => f.write_str("^")?,
3617 ZshGlobQualifier::Flag { name, .. } => write!(f, "{name}")?,
3618 ZshGlobQualifier::LetterSequence { text, .. } => {
3619 f.write_str(display_source_text(Some(text), source))?;
3620 }
3621 ZshGlobQualifier::NumericArgument { start, end, .. } => {
3622 f.write_str("[")?;
3623 f.write_str(display_source_text(Some(start), source))?;
3624 if let Some(end) = end {
3625 f.write_str(",")?;
3626 f.write_str(display_source_text(Some(end), source))?;
3627 }
3628 f.write_str("]")?;
3629 }
3630 }
3631 }
3632 f.write_str(")")
3633}
3634
3635fn operator_is_source_backed(operator: &ParameterOp) -> bool {
3636 match operator {
3637 ParameterOp::RemovePrefixShort { pattern }
3638 | ParameterOp::RemovePrefixLong { pattern }
3639 | ParameterOp::RemoveSuffixShort { pattern }
3640 | ParameterOp::RemoveSuffixLong { pattern } => pattern.is_source_backed(),
3641 ParameterOp::ReplaceFirst {
3642 pattern,
3643 replacement,
3644 ..
3645 }
3646 | ParameterOp::ReplaceAll {
3647 pattern,
3648 replacement,
3649 ..
3650 } => pattern.is_source_backed() && replacement.is_source_backed(),
3651 _ => true,
3652 }
3653}
3654
3655#[derive(Debug, Clone)]
3657pub enum ParameterOp {
3658 UseDefault,
3660 AssignDefault,
3662 UseReplacement,
3664 Error,
3666 RemovePrefixShort { pattern: Pattern },
3668 RemovePrefixLong { pattern: Pattern },
3670 RemoveSuffixShort { pattern: Pattern },
3672 RemoveSuffixLong { pattern: Pattern },
3674 ReplaceFirst {
3676 pattern: Pattern,
3677 replacement: SourceText,
3678 replacement_word_ast: Word,
3679 },
3680 ReplaceAll {
3682 pattern: Pattern,
3683 replacement: SourceText,
3684 replacement_word_ast: Word,
3685 },
3686 UpperFirst,
3688 UpperAll,
3690 LowerFirst,
3692 LowerAll,
3694}
3695
3696impl ParameterOp {
3697 pub fn replacement_word_ast(&self) -> Option<&Word> {
3698 match self {
3699 Self::ReplaceFirst {
3700 replacement_word_ast,
3701 ..
3702 }
3703 | Self::ReplaceAll {
3704 replacement_word_ast,
3705 ..
3706 } => Some(replacement_word_ast),
3707 Self::UseDefault
3708 | Self::AssignDefault
3709 | Self::UseReplacement
3710 | Self::Error
3711 | Self::RemovePrefixShort { .. }
3712 | Self::RemovePrefixLong { .. }
3713 | Self::RemoveSuffixShort { .. }
3714 | Self::RemoveSuffixLong { .. }
3715 | Self::UpperFirst
3716 | Self::UpperAll
3717 | Self::LowerFirst
3718 | Self::LowerAll => None,
3719 }
3720 }
3721}
3722
3723#[derive(Debug, Clone)]
3725pub struct Redirect {
3726 pub fd: Option<i32>,
3728 pub fd_var: Option<Name>,
3730 pub fd_var_span: Option<Span>,
3732 pub kind: RedirectKind,
3734 pub span: Span,
3736 pub target: RedirectTarget,
3738}
3739
3740impl Redirect {
3741 pub fn word_target(&self) -> Option<&Word> {
3743 match &self.target {
3744 RedirectTarget::Word(word) => Some(word),
3745 RedirectTarget::Heredoc(_) => None,
3746 }
3747 }
3748
3749 pub fn word_target_mut(&mut self) -> Option<&mut Word> {
3751 match &mut self.target {
3752 RedirectTarget::Word(word) => Some(word),
3753 RedirectTarget::Heredoc(_) => None,
3754 }
3755 }
3756
3757 pub fn heredoc(&self) -> Option<&Heredoc> {
3759 match &self.target {
3760 RedirectTarget::Word(_) => None,
3761 RedirectTarget::Heredoc(heredoc) => Some(heredoc),
3762 }
3763 }
3764
3765 pub fn heredoc_mut(&mut self) -> Option<&mut Heredoc> {
3767 match &mut self.target {
3768 RedirectTarget::Word(_) => None,
3769 RedirectTarget::Heredoc(heredoc) => Some(heredoc),
3770 }
3771 }
3772}
3773
3774#[derive(Debug, Clone)]
3776pub enum RedirectTarget {
3777 Word(Word),
3779 Heredoc(Heredoc),
3781}
3782
3783#[derive(Debug, Clone)]
3785pub struct Heredoc {
3786 pub delimiter: HeredocDelimiter,
3787 pub body: HeredocBody,
3788}
3789
3790#[derive(Debug, Clone)]
3792pub struct HeredocDelimiter {
3793 pub raw: Word,
3795 pub cooked: String,
3797 pub span: Span,
3799 pub quoted: bool,
3801 pub expands_body: bool,
3803 pub strip_tabs: bool,
3805}
3806
3807#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3809pub enum RedirectKind {
3810 Output,
3812 Clobber,
3814 Append,
3816 Input,
3818 ReadWrite,
3820 HereDoc,
3822 HereDocStrip,
3824 HereString,
3826 DupOutput,
3828 DupInput,
3830 OutputBoth,
3832}
3833
3834#[derive(Debug, Clone)]
3836pub struct Assignment {
3837 pub target: VarRef,
3838 pub value: AssignmentValue,
3839 pub append: bool,
3841 pub span: Span,
3843}
3844
3845#[derive(Debug, Clone)]
3847pub enum AssignmentValue {
3848 Scalar(Word),
3850 Compound(ArrayExpr),
3852}
3853
3854#[cfg(test)]
3855mod tests {
3856 use std::borrow::Cow;
3857
3858 use super::*;
3859
3860 fn word(parts: Vec<WordPart>) -> Word {
3861 let span = Span::new();
3862 Word {
3863 parts: parts
3864 .into_iter()
3865 .map(|part| WordPartNode::new(part, span))
3866 .collect(),
3867 span,
3868 brace_syntax: Vec::new(),
3869 }
3870 }
3871
3872 fn pattern(parts: Vec<PatternPart>) -> Pattern {
3873 let span = Span::new();
3874 Pattern {
3875 parts: parts
3876 .into_iter()
3877 .map(|part| PatternPartNode::new(part, span))
3878 .collect(),
3879 span,
3880 }
3881 }
3882
3883 fn plain_ref(name: &str) -> VarRef {
3884 let span = Span::new();
3885 VarRef {
3886 name: name.into(),
3887 name_span: span,
3888 subscript: None,
3889 span,
3890 }
3891 }
3892
3893 fn indexed_ref(name: &str, index: &str) -> VarRef {
3894 let span = Span::new();
3895 VarRef {
3896 name: name.into(),
3897 name_span: span,
3898 subscript: Some(Subscript {
3899 text: index.into(),
3900 raw: None,
3901 kind: SubscriptKind::Ordinary,
3902 interpretation: SubscriptInterpretation::Contextual,
3903 word_ast: None,
3904 arithmetic_ast: None,
3905 }),
3906 span,
3907 }
3908 }
3909
3910 fn selector_ref(name: &str, selector: SubscriptSelector) -> VarRef {
3911 let span = Span::new();
3912 VarRef {
3913 name: name.into(),
3914 name_span: span,
3915 subscript: Some(Subscript {
3916 text: selector.as_char().to_string().into(),
3917 raw: None,
3918 kind: SubscriptKind::Selector(selector),
3919 interpretation: SubscriptInterpretation::Contextual,
3920 word_ast: None,
3921 arithmetic_ast: None,
3922 }),
3923 span,
3924 }
3925 }
3926
3927 fn assignment(target: VarRef, value: AssignmentValue) -> Assignment {
3928 Assignment {
3929 target,
3930 value,
3931 append: false,
3932 span: Span::new(),
3933 }
3934 }
3935
3936 fn stmt(command: Command) -> Stmt {
3937 Stmt {
3938 leading_comments: vec![],
3939 command,
3940 negated: false,
3941 redirects: Box::default(),
3942 terminator: None,
3943 terminator_span: None,
3944 inline_comment: None,
3945 span: Span::new(),
3946 }
3947 }
3948
3949 fn stmt_with_redirects(command: Command, redirects: Vec<Redirect>) -> Stmt {
3950 Stmt {
3951 redirects: redirects.into_boxed_slice(),
3952 ..stmt(command)
3953 }
3954 }
3955
3956 fn stmt_seq(stmts: Vec<Stmt>) -> StmtSeq {
3957 StmtSeq {
3958 leading_comments: vec![],
3959 stmts,
3960 trailing_comments: vec![],
3961 span: Span::new(),
3962 }
3963 }
3964
3965 fn simple_command(name: &str, args: Vec<Word>) -> SimpleCommand {
3966 SimpleCommand {
3967 name: Word::literal(name),
3968 args,
3969 assignments: Box::default(),
3970 span: Span::new(),
3971 }
3972 }
3973
3974 fn simple_stmt(name: &str, args: Vec<Word>) -> Stmt {
3975 stmt(Command::Simple(simple_command(name, args)))
3976 }
3977
3978 #[test]
3979 fn word_try_static_text_borrows_simple_static_words() {
3980 assert!(matches!(
3981 Word::literal("plain").try_static_text(""),
3982 Some(Cow::Borrowed("plain"))
3983 ));
3984
3985 let single_quoted = word(vec![WordPart::SingleQuoted {
3986 value: "single".into(),
3987 dollar: false,
3988 }]);
3989 assert!(matches!(
3990 single_quoted.try_static_text(""),
3991 Some(Cow::Borrowed("single"))
3992 ));
3993 }
3994
3995 #[test]
3996 fn word_try_static_text_concatenates_nested_static_parts() {
3997 let span = Span::new();
3998 let word = Word {
3999 parts: vec![
4000 WordPartNode::new(WordPart::Literal(LiteralText::owned("foo")), span),
4001 WordPartNode::new(
4002 WordPart::DoubleQuoted {
4003 parts: vec![WordPartNode::new(
4004 WordPart::Literal(LiteralText::owned("bar")),
4005 span,
4006 )],
4007 dollar: false,
4008 },
4009 span,
4010 ),
4011 ],
4012 span,
4013 brace_syntax: Vec::new(),
4014 };
4015
4016 assert!(matches!(
4017 word.try_static_text(""),
4018 Some(Cow::Owned(ref value)) if value == "foobar"
4019 ));
4020 }
4021
4022 #[test]
4023 fn word_try_static_text_rejects_runtime_expansions() {
4024 let variable = word(vec![WordPart::Variable("name".into())]);
4025 assert!(variable.try_static_text("").is_none());
4026 }
4027
4028 #[test]
4029 fn command_name_text_decodes_unquoted_backslashes() {
4030 assert_eq!(
4031 decode_static_command_literal("\\foo\\ bar\\\nqux", StaticCommandNameContext::Unquoted)
4032 .as_ref(),
4033 "foo barqux"
4034 );
4035 }
4036
4037 #[test]
4038 fn command_name_text_decodes_double_quoted_backslashes_selectively() {
4039 assert_eq!(
4040 decode_static_command_literal("\\$foo\\q\\\\", StaticCommandNameContext::DoubleQuoted)
4041 .as_ref(),
4042 "$foo\\q\\"
4043 );
4044 }
4045
4046 #[test]
4047 fn command_name_text_concatenates_nested_static_parts() {
4048 let span = Span::new();
4049 let word = Word {
4050 parts: vec![
4051 WordPartNode::new(WordPart::Literal(LiteralText::owned("\\foo")), span),
4052 WordPartNode::new(
4053 WordPart::DoubleQuoted {
4054 parts: vec![WordPartNode::new(
4055 WordPart::Literal(LiteralText::owned("\\$bar")),
4056 span,
4057 )],
4058 dollar: false,
4059 },
4060 span,
4061 ),
4062 ],
4063 span,
4064 brace_syntax: Vec::new(),
4065 };
4066
4067 assert_eq!(
4068 static_command_name_text(&word, "").as_deref(),
4069 Some("foo$bar")
4070 );
4071 }
4072
4073 #[test]
4074 fn shell_variable_name_helper_matches_identifier_rules() {
4075 assert!(is_shell_variable_name("name"));
4076 assert!(is_shell_variable_name("_name123"));
4077 assert!(!is_shell_variable_name("1name"));
4078 assert!(!is_shell_variable_name("name-value"));
4079 }
4080
4081 #[test]
4082 fn word_is_standalone_variable_like_matches_single_expansion_words() {
4083 assert!(word_is_standalone_variable_like(&word(vec![
4084 WordPart::Variable("name".into())
4085 ])));
4086 assert!(word_is_standalone_variable_like(&word(vec![
4087 WordPart::Length(plain_ref("name"))
4088 ])));
4089 assert!(!word_is_standalone_variable_like(&word(vec![
4090 WordPart::Literal(LiteralText::owned("prefix")),
4091 WordPart::Variable("name".into()),
4092 ])));
4093 assert!(!word_is_standalone_variable_like(&word(vec![
4094 WordPart::DoubleQuoted {
4095 parts: vec![WordPartNode::new(
4096 WordPart::Variable("name".into()),
4097 Span::new(),
4098 )],
4099 dollar: false,
4100 }
4101 ])));
4102 }
4103
4104 #[test]
4105 fn word_is_standalone_status_capture_handles_plain_quoted_and_parameter_forms() {
4106 assert!(word_is_standalone_status_capture(&word(vec![
4107 WordPart::Variable("?".into())
4108 ])));
4109 assert!(word_is_standalone_status_capture(&word(vec![
4110 WordPart::DoubleQuoted {
4111 parts: vec![WordPartNode::new(
4112 WordPart::Variable("?".into()),
4113 Span::new(),
4114 )],
4115 dollar: false,
4116 }
4117 ])));
4118 assert!(word_is_standalone_status_capture(&word(vec![
4119 WordPart::Parameter(ParameterExpansion {
4120 syntax: ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
4121 reference: plain_ref("?"),
4122 }),
4123 span: Span::new(),
4124 raw_body: "?".into(),
4125 })
4126 ])));
4127 assert!(!word_is_standalone_status_capture(&word(vec![
4128 WordPart::Variable("name".into())
4129 ])));
4130 assert!(!word_is_standalone_status_capture(&word(vec![
4131 WordPart::Literal(LiteralText::owned("status=")),
4132 WordPart::Variable("?".into()),
4133 ])));
4134 }
4135
4136 fn span_for_source(source: &str) -> Span {
4137 Span::from_positions(
4138 Position {
4139 line: 1,
4140 column: 1,
4141 offset: 0,
4142 },
4143 Position {
4144 line: 1,
4145 column: source.chars().count() + 1,
4146 offset: source.len(),
4147 },
4148 )
4149 }
4150
4151 #[test]
4154 fn word_literal_creates_unquoted_word() {
4155 let w = Word::literal("hello");
4156 assert_eq!(w.parts.len(), 1);
4157 assert!(matches!(w.part(0), Some(WordPart::Literal(s)) if s == "hello"));
4158 }
4159
4160 #[test]
4161 fn word_literal_empty_string() {
4162 let w = Word::literal("");
4163 assert!(matches!(w.part(0), Some(WordPart::Literal(s)) if s.is_empty()));
4164 }
4165
4166 #[test]
4167 fn literal_text_owned_compares_equal_to_str() {
4168 let text = LiteralText::owned("hello");
4169
4170 assert!(text == "hello");
4171 assert!(text != "world");
4172 }
4173
4174 #[test]
4175 fn literal_text_source_does_not_compare_equal_to_str_without_source() {
4176 let text = LiteralText::source();
4177
4178 assert!(!text.is_empty());
4179 assert!(text != "hello");
4180 }
4181
4182 #[test]
4183 fn literal_text_eq_str_uses_source_for_source_backed_literals() {
4184 let source = "hello";
4185 let span = span_for_source(source);
4186 let text = LiteralText::source();
4187
4188 assert!(text.eq_str(source, span, "hello"));
4189 assert!(!text.eq_str(source, span, "world"));
4190 }
4191
4192 #[test]
4193 fn word_quoted_literal_creates_single_quoted_part() {
4194 let w = Word::quoted_literal("world");
4195 assert_eq!(w.parts.len(), 1);
4196 assert!(matches!(
4197 w.part(0),
4198 Some(WordPart::SingleQuoted { dollar: false, .. })
4199 ));
4200 assert_eq!(format!("{w}"), "world");
4201 }
4202
4203 #[test]
4204 fn word_display_literal() {
4205 let w = Word::literal("echo");
4206 assert_eq!(format!("{w}"), "echo");
4207 }
4208
4209 #[test]
4210 fn word_render_syntax_preserves_cooked_double_quoted_literal() {
4211 let w = word(vec![WordPart::DoubleQuoted {
4212 parts: vec![WordPartNode::new(
4213 WordPart::Literal(LiteralText::owned("hello".to_string())),
4214 Span::new(),
4215 )],
4216 dollar: false,
4217 }]);
4218 assert_eq!(w.render_syntax(""), "\"hello\"");
4219 }
4220
4221 #[test]
4222 fn word_render_syntax_reescapes_cooked_double_quoted_literal_text() {
4223 let w = word(vec![WordPart::DoubleQuoted {
4224 parts: vec![WordPartNode::new(
4225 WordPart::Literal(LiteralText::owned(
4226 "quoted \"value\" uses $HOME and `pwd` with \\".to_string(),
4227 )),
4228 Span::new(),
4229 )],
4230 dollar: false,
4231 }]);
4232
4233 assert_eq!(
4234 w.render_syntax(""),
4235 "\"quoted \\\"value\\\" uses \\$HOME and \\`pwd\\` with \\\\\""
4236 );
4237 }
4238
4239 #[test]
4240 fn word_render_syntax_preserves_nested_parameter_expansion_inside_double_quotes() {
4241 let w = word(vec![WordPart::DoubleQuoted {
4242 parts: vec![
4243 WordPartNode::new(
4244 WordPart::Literal(LiteralText::owned("N/A: version \"".to_string())),
4245 Span::new(),
4246 ),
4247 WordPartNode::new(
4248 WordPart::ParameterExpansion {
4249 reference: plain_ref("PREFIXED_VERSION"),
4250 operator: ParameterOp::UseDefault,
4251 operand: Some("$PROVIDED_VERSION".into()),
4252 operand_word_ast: Some(word(vec![WordPart::Variable(
4253 "PROVIDED_VERSION".into(),
4254 )])),
4255 colon_variant: true,
4256 },
4257 Span::new(),
4258 ),
4259 WordPartNode::new(
4260 WordPart::Literal(LiteralText::owned("\" is not yet installed.".to_string())),
4261 Span::new(),
4262 ),
4263 ],
4264 dollar: false,
4265 }]);
4266
4267 assert_eq!(
4268 w.render_syntax(""),
4269 "\"N/A: version \\\"${PREFIXED_VERSION:-$PROVIDED_VERSION}\\\" is not yet installed.\""
4270 );
4271 }
4272
4273 #[test]
4274 fn word_render_syntax_preserves_source_backed_braced_variable() {
4275 let span = Span::from_positions(
4276 Position {
4277 line: 1,
4278 column: 1,
4279 offset: 0,
4280 },
4281 Position {
4282 line: 1,
4283 column: 5,
4284 offset: 4,
4285 },
4286 );
4287 let w = Word {
4288 parts: vec![WordPartNode::new(WordPart::Variable("1".into()), span)],
4289 span,
4290 brace_syntax: Vec::new(),
4291 };
4292
4293 assert_eq!(w.render_syntax("${1}"), "${1}");
4294 }
4295
4296 #[test]
4297 fn word_render_syntax_trims_source_backed_literal_delimiters() {
4298 let span = Span::from_positions(
4299 Position {
4300 line: 1,
4301 column: 1,
4302 offset: 0,
4303 },
4304 Position {
4305 line: 1,
4306 column: 5,
4307 offset: 4,
4308 },
4309 );
4310 let w = Word {
4311 parts: vec![WordPartNode::new(
4312 WordPart::Literal(LiteralText::source()),
4313 span,
4314 )],
4315 span,
4316 brace_syntax: Vec::new(),
4317 };
4318
4319 assert_eq!(w.render_syntax("foo "), "foo");
4320 }
4321
4322 #[test]
4323 fn word_render_syntax_prefers_whole_word_source_slice() {
4324 let source = "\"source \\\"$fzf_base/shell/completion.${shell}\\\"\"";
4325 let span = span_for_source(source);
4326 let w = Word {
4327 parts: vec![WordPartNode::new(
4328 WordPart::DoubleQuoted {
4329 parts: vec![WordPartNode::new(
4330 WordPart::Literal(LiteralText::owned(
4331 "source \"$fzf_base/shell/completion.${shell}\"".to_string(),
4332 )),
4333 span,
4334 )],
4335 dollar: false,
4336 },
4337 span,
4338 )],
4339 span,
4340 brace_syntax: Vec::new(),
4341 };
4342
4343 assert_eq!(w.render_syntax(source), source);
4344 }
4345
4346 #[test]
4347 fn word_render_to_buf_appends_to_existing_contents() {
4348 let word = word(vec![
4349 WordPart::Literal("hello ".into()),
4350 WordPart::Variable("USER".into()),
4351 ]);
4352 let mut rendered = String::from("prefix:");
4353
4354 word.render_to_buf("hello $USER", &mut rendered);
4355
4356 assert_eq!(rendered, "prefix:hello $USER");
4357 assert_eq!(rendered["prefix:".len()..], word.render("hello $USER"));
4358 }
4359
4360 #[test]
4361 fn word_render_syntax_to_buf_matches_render_syntax() {
4362 let source = "\"hello\"";
4363 let span = span_for_source(source);
4364 let word = Word {
4365 parts: vec![WordPartNode::new(
4366 WordPart::DoubleQuoted {
4367 parts: vec![WordPartNode::new(
4368 WordPart::Literal(LiteralText::owned("hello".to_string())),
4369 span,
4370 )],
4371 dollar: false,
4372 },
4373 span,
4374 )],
4375 span,
4376 brace_syntax: Vec::new(),
4377 };
4378 let mut rendered = String::from("prefix:");
4379
4380 word.render_syntax_to_buf(source, &mut rendered);
4381
4382 assert_eq!(rendered, format!("prefix:{}", word.render_syntax(source)));
4383 }
4384
4385 #[test]
4386 fn word_display_variable() {
4387 let w = word(vec![WordPart::Variable("HOME".into())]);
4388 assert_eq!(format!("{w}"), "$HOME");
4389 }
4390
4391 #[test]
4392 fn word_display_arithmetic_expansion() {
4393 let w = word(vec![WordPart::ArithmeticExpansion {
4394 expression: "1+2".into(),
4395 expression_ast: None,
4396 expression_word_ast: Word::literal("1+2"),
4397 syntax: ArithmeticExpansionSyntax::DollarParenParen,
4398 }]);
4399 assert_eq!(format!("{w}"), "$((1+2))");
4400 }
4401
4402 #[test]
4403 fn word_display_length() {
4404 let w = word(vec![WordPart::Length(plain_ref("var"))]);
4405 assert_eq!(format!("{w}"), "${#var}");
4406 }
4407
4408 #[test]
4409 fn word_display_array_access() {
4410 let w = word(vec![WordPart::ArrayAccess(indexed_ref("arr", "0"))]);
4411 assert_eq!(format!("{w}"), "${arr[0]}");
4412 }
4413
4414 #[test]
4415 fn word_display_array_length() {
4416 let w = word(vec![WordPart::ArrayLength(selector_ref(
4417 "arr",
4418 SubscriptSelector::At,
4419 ))]);
4420 assert_eq!(format!("{w}"), "${#arr[@]}");
4421 }
4422
4423 #[test]
4424 fn word_display_array_indices() {
4425 let w = word(vec![WordPart::ArrayIndices(selector_ref(
4426 "arr",
4427 SubscriptSelector::At,
4428 ))]);
4429 assert_eq!(format!("{w}"), "${!arr[@]}");
4430 }
4431
4432 #[test]
4433 fn word_display_substring_with_length() {
4434 let w = word(vec![WordPart::Substring {
4435 reference: plain_ref("var"),
4436 offset: "2".into(),
4437 offset_ast: None,
4438 offset_word_ast: Word::literal("2"),
4439 length: Some("3".into()),
4440 length_ast: None,
4441 length_word_ast: Some(Word::literal("3")),
4442 }]);
4443 assert_eq!(format!("{w}"), "${var:2:3}");
4444 }
4445
4446 #[test]
4447 fn word_display_substring_without_length() {
4448 let w = word(vec![WordPart::Substring {
4449 reference: plain_ref("var"),
4450 offset: "2".into(),
4451 offset_ast: None,
4452 offset_word_ast: Word::literal("2"),
4453 length: None,
4454 length_ast: None,
4455 length_word_ast: None,
4456 }]);
4457 assert_eq!(format!("{w}"), "${var:2}");
4458 }
4459
4460 #[test]
4461 fn word_display_array_slice_with_length() {
4462 let w = word(vec![WordPart::ArraySlice {
4463 reference: selector_ref("arr", SubscriptSelector::At),
4464 offset: "1".into(),
4465 offset_ast: None,
4466 offset_word_ast: Word::literal("1"),
4467 length: Some("2".into()),
4468 length_ast: None,
4469 length_word_ast: Some(Word::literal("2")),
4470 }]);
4471 assert_eq!(format!("{w}"), "${arr[@]:1:2}");
4472 }
4473
4474 #[test]
4475 fn word_display_array_slice_without_length() {
4476 let w = word(vec![WordPart::ArraySlice {
4477 reference: selector_ref("arr", SubscriptSelector::At),
4478 offset: "1".into(),
4479 offset_ast: None,
4480 offset_word_ast: Word::literal("1"),
4481 length: None,
4482 length_ast: None,
4483 length_word_ast: None,
4484 }]);
4485 assert_eq!(format!("{w}"), "${arr[@]:1}");
4486 }
4487
4488 #[test]
4489 fn word_display_indirect_expansion() {
4490 let w = word(vec![WordPart::IndirectExpansion {
4491 reference: plain_ref("ref"),
4492 operator: None,
4493 operand: None,
4494 operand_word_ast: None,
4495 colon_variant: false,
4496 }]);
4497 assert_eq!(format!("{w}"), "${!ref}");
4498 }
4499
4500 #[test]
4501 fn word_display_prefix_match() {
4502 let w = word(vec![WordPart::PrefixMatch {
4503 prefix: "MY_".into(),
4504 kind: PrefixMatchKind::Star,
4505 }]);
4506 assert_eq!(format!("{w}"), "${!MY_*}");
4507 }
4508
4509 #[test]
4510 fn word_display_prefix_match_at() {
4511 let w = word(vec![WordPart::PrefixMatch {
4512 prefix: "MY_".into(),
4513 kind: PrefixMatchKind::At,
4514 }]);
4515 assert_eq!(format!("{w}"), "${!MY_@}");
4516 }
4517
4518 #[test]
4519 fn word_render_syntax_preserves_raw_quoted_subscript() {
4520 let w = word(vec![WordPart::ArrayAccess(VarRef {
4521 name: "assoc".into(),
4522 name_span: Span::new(),
4523 subscript: Some(Subscript {
4524 text: "key".into(),
4525 raw: Some("\"key\"".into()),
4526 kind: SubscriptKind::Ordinary,
4527 interpretation: SubscriptInterpretation::Associative,
4528 word_ast: None,
4529 arithmetic_ast: None,
4530 }),
4531 span: Span::new(),
4532 })]);
4533 assert_eq!(format!("{w}"), "${assoc[\"key\"]}");
4534 assert_eq!(w.render_syntax(""), "${assoc[\"key\"]}");
4535 }
4536
4537 #[test]
4538 fn word_display_transformation() {
4539 let w = word(vec![WordPart::Transformation {
4540 reference: plain_ref("var"),
4541 operator: 'Q',
4542 }]);
4543 assert_eq!(format!("{w}"), "${var@Q}");
4544 }
4545
4546 #[test]
4547 fn word_display_multiple_parts() {
4548 let w = word(vec![
4549 WordPart::Literal("hello ".into()),
4550 WordPart::Variable("USER".into()),
4551 ]);
4552 assert_eq!(format!("{w}"), "hello $USER");
4553 }
4554
4555 #[test]
4556 fn pattern_display_multiple_parts() {
4557 let p = pattern(vec![
4558 PatternPart::Literal("file".into()),
4559 PatternPart::AnyString,
4560 PatternPart::CharClass("[[:digit:]]".into()),
4561 ]);
4562 assert_eq!(format!("{p}"), "file*[[:digit:]]");
4563 }
4564
4565 #[test]
4566 fn pattern_render_syntax_prefers_whole_pattern_source_slice() {
4567 let source = "Darwin\\ arm64*";
4568 let span = span_for_source(source);
4569 let p = Pattern {
4570 parts: vec![PatternPartNode::new(
4571 PatternPart::Literal(LiteralText::owned("Darwin arm64*".to_string())),
4572 span,
4573 )],
4574 span,
4575 };
4576
4577 assert_eq!(p.render_syntax(source), source);
4578 }
4579
4580 #[test]
4581 fn pattern_render_to_buf_appends_to_existing_contents() {
4582 let pattern = pattern(vec![
4583 PatternPart::Literal("file".into()),
4584 PatternPart::AnyString,
4585 PatternPart::CharClass("[[:digit:]]".into()),
4586 ]);
4587 let source = "file*[[:digit:]]";
4588 let mut rendered = String::from("prefix:");
4589
4590 pattern.render_to_buf(source, &mut rendered);
4591
4592 assert_eq!(rendered, format!("prefix:{}", pattern.render(source)));
4593 }
4594
4595 #[test]
4596 fn pattern_render_syntax_to_buf_matches_render_syntax() {
4597 let source = "Darwin\\ arm64*";
4598 let span = span_for_source(source);
4599 let pattern = Pattern {
4600 parts: vec![PatternPartNode::new(
4601 PatternPart::Literal(LiteralText::owned("Darwin arm64*".to_string())),
4602 span,
4603 )],
4604 span,
4605 };
4606 let mut rendered = String::from("prefix:");
4607
4608 pattern.render_syntax_to_buf(source, &mut rendered);
4609
4610 assert_eq!(
4611 rendered,
4612 format!("prefix:{}", pattern.render_syntax(source))
4613 );
4614 }
4615
4616 #[test]
4617 fn pattern_display_extglob_group() {
4618 let p = pattern(vec![PatternPart::Group {
4619 kind: PatternGroupKind::ExactlyOne,
4620 patterns: vec![
4621 pattern(vec![PatternPart::Literal("foo".into())]),
4622 pattern(vec![PatternPart::Literal("bar".into())]),
4623 ],
4624 }]);
4625 assert_eq!(format!("{p}"), "@(foo|bar)");
4626 }
4627
4628 #[test]
4629 fn word_display_parameter_expansion_use_default_colon() {
4630 let w = word(vec![WordPart::ParameterExpansion {
4631 reference: plain_ref("var"),
4632 operator: ParameterOp::UseDefault,
4633 operand: Some("fallback".into()),
4634 operand_word_ast: Some(Word::literal("fallback")),
4635 colon_variant: true,
4636 }]);
4637 assert_eq!(format!("{w}"), "${var:-fallback}");
4638 }
4639
4640 #[test]
4641 fn word_display_parameter_expansion_use_default_no_colon() {
4642 let w = word(vec![WordPart::ParameterExpansion {
4643 reference: plain_ref("var"),
4644 operator: ParameterOp::UseDefault,
4645 operand: Some("fallback".into()),
4646 operand_word_ast: Some(Word::literal("fallback")),
4647 colon_variant: false,
4648 }]);
4649 assert_eq!(format!("{w}"), "${var-fallback}");
4650 }
4651
4652 #[test]
4653 fn word_display_parameter_expansion_assign_default() {
4654 let w = word(vec![WordPart::ParameterExpansion {
4655 reference: plain_ref("var"),
4656 operator: ParameterOp::AssignDefault,
4657 operand: Some("val".into()),
4658 operand_word_ast: Some(Word::literal("val")),
4659 colon_variant: true,
4660 }]);
4661 assert_eq!(format!("{w}"), "${var:=val}");
4662 }
4663
4664 #[test]
4665 fn word_display_parameter_expansion_use_replacement() {
4666 let w = word(vec![WordPart::ParameterExpansion {
4667 reference: plain_ref("var"),
4668 operator: ParameterOp::UseReplacement,
4669 operand: Some("alt".into()),
4670 operand_word_ast: Some(Word::literal("alt")),
4671 colon_variant: true,
4672 }]);
4673 assert_eq!(format!("{w}"), "${var:+alt}");
4674 }
4675
4676 #[test]
4677 fn word_display_parameter_expansion_error() {
4678 let w = word(vec![WordPart::ParameterExpansion {
4679 reference: plain_ref("var"),
4680 operator: ParameterOp::Error,
4681 operand: Some("msg".into()),
4682 operand_word_ast: Some(Word::literal("msg")),
4683 colon_variant: true,
4684 }]);
4685 assert_eq!(format!("{w}"), "${var:?msg}");
4686 }
4687
4688 #[test]
4689 fn word_display_parameter_expansion_prefix_suffix() {
4690 let w = word(vec![WordPart::ParameterExpansion {
4692 reference: plain_ref("var"),
4693 operator: ParameterOp::RemovePrefixShort {
4694 pattern: pattern(vec![PatternPart::Literal("pat".into())]),
4695 },
4696 operand: None,
4697 operand_word_ast: None,
4698 colon_variant: false,
4699 }]);
4700 assert_eq!(format!("{w}"), "${var#pat}");
4701
4702 let w = word(vec![WordPart::ParameterExpansion {
4704 reference: plain_ref("var"),
4705 operator: ParameterOp::RemovePrefixLong {
4706 pattern: pattern(vec![PatternPart::Literal("pat".into())]),
4707 },
4708 operand: None,
4709 operand_word_ast: None,
4710 colon_variant: false,
4711 }]);
4712 assert_eq!(format!("{w}"), "${var##pat}");
4713
4714 let w = word(vec![WordPart::ParameterExpansion {
4716 reference: plain_ref("var"),
4717 operator: ParameterOp::RemoveSuffixShort {
4718 pattern: pattern(vec![PatternPart::Literal("pat".into())]),
4719 },
4720 operand: None,
4721 operand_word_ast: None,
4722 colon_variant: false,
4723 }]);
4724 assert_eq!(format!("{w}"), "${var%pat}");
4725
4726 let w = word(vec![WordPart::ParameterExpansion {
4728 reference: plain_ref("var"),
4729 operator: ParameterOp::RemoveSuffixLong {
4730 pattern: pattern(vec![PatternPart::Literal("pat".into())]),
4731 },
4732 operand: None,
4733 operand_word_ast: None,
4734 colon_variant: false,
4735 }]);
4736 assert_eq!(format!("{w}"), "${var%%pat}");
4737 }
4738
4739 #[test]
4740 fn word_display_parameter_expansion_replace() {
4741 let w = word(vec![WordPart::ParameterExpansion {
4742 reference: plain_ref("var"),
4743 operator: ParameterOp::ReplaceFirst {
4744 pattern: pattern(vec![PatternPart::Literal("old".into())]),
4745 replacement: "new".into(),
4746 replacement_word_ast: Word::literal("new"),
4747 },
4748 operand: None,
4749 operand_word_ast: None,
4750 colon_variant: false,
4751 }]);
4752 assert_eq!(format!("{w}"), "${var/old/new}");
4753
4754 let w = word(vec![WordPart::ParameterExpansion {
4755 reference: plain_ref("var"),
4756 operator: ParameterOp::ReplaceAll {
4757 pattern: pattern(vec![PatternPart::Literal("old".into())]),
4758 replacement: "new".into(),
4759 replacement_word_ast: Word::literal("new"),
4760 },
4761 operand: None,
4762 operand_word_ast: None,
4763 colon_variant: false,
4764 }]);
4765 assert_eq!(format!("{w}"), "${var//old/new}");
4766 }
4767
4768 #[test]
4769 fn word_display_parameter_expansion_case() {
4770 let check = |op: ParameterOp, expected: &str| {
4771 let w = word(vec![WordPart::ParameterExpansion {
4772 reference: plain_ref("var"),
4773 operator: op,
4774 operand: None,
4775 operand_word_ast: None,
4776 colon_variant: false,
4777 }]);
4778 assert_eq!(format!("{w}"), expected);
4779 };
4780 check(ParameterOp::UpperFirst, "${var^}");
4781 check(ParameterOp::UpperAll, "${var^^}");
4782 check(ParameterOp::LowerAll, "${var,,}");
4783 }
4784
4785 #[test]
4788 fn simple_command_construction() {
4789 let cmd = simple_command("ls", vec![Word::literal("-la")]);
4790 assert_eq!(format!("{}", cmd.name), "ls");
4791 assert_eq!(cmd.args.len(), 1);
4792 assert_eq!(format!("{}", cmd.args[0]), "-la");
4793 }
4794
4795 #[test]
4796 fn statement_redirects_are_stored_on_stmt() {
4797 let cmd = stmt_with_redirects(
4798 Command::Simple(simple_command("echo", vec![Word::literal("hi")])),
4799 vec![Redirect {
4800 fd: Some(1),
4801 fd_var: None,
4802 fd_var_span: None,
4803 kind: RedirectKind::Output,
4804 span: Span::new(),
4805 target: RedirectTarget::Word(Word::literal("out.txt")),
4806 }],
4807 );
4808 assert_eq!(cmd.redirects.len(), 1);
4809 assert_eq!(cmd.redirects[0].fd, Some(1));
4810 assert_eq!(cmd.redirects[0].kind, RedirectKind::Output);
4811 }
4812
4813 #[test]
4814 fn simple_command_with_assignments() {
4815 let cmd = SimpleCommand {
4816 assignments: vec![assignment(
4817 plain_ref("FOO"),
4818 AssignmentValue::Scalar(Word::literal("bar")),
4819 )]
4820 .into_boxed_slice(),
4821 ..simple_command("env", vec![])
4822 };
4823 assert_eq!(cmd.assignments.len(), 1);
4824 assert_eq!(cmd.assignments[0].target.name, "FOO");
4825 assert!(!cmd.assignments[0].append);
4826 }
4827
4828 #[test]
4831 fn builtin_break_command_construction() {
4832 let cmd = BuiltinCommand::Break(BreakCommand {
4833 depth: Some(Word::literal("2")),
4834 extra_args: vec![Word::literal("extra")],
4835 assignments: Box::default(),
4836 span: Span::new(),
4837 });
4838
4839 if let BuiltinCommand::Break(command) = &cmd {
4840 assert_eq!(command.depth.as_ref().unwrap().to_string(), "2");
4841 assert_eq!(command.extra_args.len(), 1);
4842 assert_eq!(command.extra_args[0].to_string(), "extra");
4843 } else {
4844 panic!("expected Break builtin");
4845 }
4846 }
4847
4848 #[test]
4849 fn builtin_return_command_with_redirects_and_assignments() {
4850 let cmd = stmt_with_redirects(
4851 Command::Builtin(BuiltinCommand::Return(ReturnCommand {
4852 code: Some(Word::literal("42")),
4853 extra_args: vec![],
4854 assignments: vec![assignment(
4855 plain_ref("FOO"),
4856 AssignmentValue::Scalar(Word::literal("bar")),
4857 )]
4858 .into_boxed_slice(),
4859 span: Span::new(),
4860 })),
4861 vec![Redirect {
4862 fd: None,
4863 fd_var: None,
4864 fd_var_span: None,
4865 kind: RedirectKind::Output,
4866 span: Span::new(),
4867 target: RedirectTarget::Word(Word::literal("out.txt")),
4868 }],
4869 );
4870
4871 if let Command::Builtin(BuiltinCommand::Return(command)) = &cmd.command {
4872 assert_eq!(command.code.as_ref().unwrap().to_string(), "42");
4873 assert_eq!(command.assignments.len(), 1);
4874 assert_eq!(cmd.redirects.len(), 1);
4875 } else {
4876 panic!("expected Return builtin");
4877 }
4878 }
4879
4880 #[test]
4883 fn binary_command_construction() {
4884 let pipe = BinaryCommand {
4885 left: Box::new(simple_stmt("ls", vec![])),
4886 op: BinaryOp::Pipe,
4887 op_span: Span::new(),
4888 right: Box::new(simple_stmt("grep", vec![Word::literal("foo")])),
4889 span: Span::new(),
4890 };
4891 assert_eq!(pipe.op, BinaryOp::Pipe);
4892 assert!(matches!(pipe.left.command, Command::Simple(_)));
4893 assert!(matches!(pipe.right.command, Command::Simple(_)));
4894 }
4895
4896 #[test]
4897 fn stmt_negated() {
4898 let mut command = simple_stmt("echo", vec![Word::literal("hi")]);
4899 command.negated = true;
4900 assert!(command.negated);
4901 }
4902
4903 #[test]
4906 fn stmt_seq_with_multiple_statements() {
4907 let list = stmt_seq(vec![
4908 simple_stmt("true", vec![]),
4909 simple_stmt("echo", vec![Word::literal("ok")]),
4910 ]);
4911 assert_eq!(list.len(), 2);
4912 assert!(matches!(list[0].command, Command::Simple(_)));
4913 }
4914
4915 #[test]
4918 fn statement_operators_equality() {
4919 assert_eq!(BinaryOp::And, BinaryOp::And);
4920 assert_eq!(BinaryOp::Or, BinaryOp::Or);
4921 assert_eq!(BinaryOp::Pipe, BinaryOp::Pipe);
4922 assert_eq!(BinaryOp::PipeAll, BinaryOp::PipeAll);
4923 assert_ne!(BinaryOp::And, BinaryOp::Or);
4924 assert_eq!(StmtTerminator::Semicolon, StmtTerminator::Semicolon);
4925 assert_eq!(
4926 StmtTerminator::Background(BackgroundOperator::Plain),
4927 StmtTerminator::Background(BackgroundOperator::Plain)
4928 );
4929 }
4930
4931 #[test]
4934 fn redirect_kind_equality() {
4935 assert_eq!(RedirectKind::Output, RedirectKind::Output);
4936 assert_eq!(RedirectKind::Append, RedirectKind::Append);
4937 assert_eq!(RedirectKind::Input, RedirectKind::Input);
4938 assert_eq!(RedirectKind::ReadWrite, RedirectKind::ReadWrite);
4939 assert_eq!(RedirectKind::HereDoc, RedirectKind::HereDoc);
4940 assert_eq!(RedirectKind::HereDocStrip, RedirectKind::HereDocStrip);
4941 assert_eq!(RedirectKind::HereString, RedirectKind::HereString);
4942 assert_eq!(RedirectKind::DupOutput, RedirectKind::DupOutput);
4943 assert_eq!(RedirectKind::DupInput, RedirectKind::DupInput);
4944 assert_eq!(RedirectKind::OutputBoth, RedirectKind::OutputBoth);
4945 assert_ne!(RedirectKind::Output, RedirectKind::Append);
4946 }
4947
4948 #[test]
4951 fn redirect_default_fd_none() {
4952 let r = Redirect {
4953 fd: None,
4954 fd_var: None,
4955 fd_var_span: None,
4956 kind: RedirectKind::Input,
4957 span: Span::new(),
4958 target: RedirectTarget::Word(Word::literal("input.txt")),
4959 };
4960 assert!(r.fd.is_none());
4961 assert_eq!(r.kind, RedirectKind::Input);
4962 }
4963
4964 #[test]
4965 fn redirect_exposes_word_target() {
4966 let redirect = Redirect {
4967 fd: None,
4968 fd_var: None,
4969 fd_var_span: None,
4970 kind: RedirectKind::Output,
4971 span: Span::new(),
4972 target: RedirectTarget::Word(Word::literal("out.txt")),
4973 };
4974
4975 assert_eq!(redirect.word_target().unwrap().to_string(), "out.txt");
4976 assert!(redirect.heredoc().is_none());
4977 }
4978
4979 #[test]
4980 fn redirect_exposes_heredoc_payload() {
4981 let delimiter = HeredocDelimiter {
4982 raw: Word::quoted_literal("EOF"),
4983 cooked: "EOF".to_owned(),
4984 span: Span::new(),
4985 quoted: true,
4986 expands_body: false,
4987 strip_tabs: false,
4988 };
4989 let redirect = Redirect {
4990 fd: None,
4991 fd_var: None,
4992 fd_var_span: None,
4993 kind: RedirectKind::HereDoc,
4994 span: Span::new(),
4995 target: RedirectTarget::Heredoc(Heredoc {
4996 delimiter,
4997 body: HeredocBody::literal_with_span("body", Span::new())
4998 .with_mode(HeredocBodyMode::Literal),
4999 }),
5000 };
5001
5002 let heredoc = redirect.heredoc().expect("expected heredoc payload");
5003 assert_eq!(heredoc.delimiter.cooked, "EOF");
5004 assert!(heredoc.delimiter.quoted);
5005 assert!(redirect.word_target().is_none());
5006 }
5007
5008 #[test]
5011 fn assignment_scalar() {
5012 let a = assignment(plain_ref("X"), AssignmentValue::Scalar(Word::literal("1")));
5013 assert_eq!(a.target.name, "X");
5014 assert!(a.target.subscript.is_none());
5015 assert!(!a.append);
5016 }
5017
5018 #[test]
5019 fn assignment_array() {
5020 let a = assignment(
5021 plain_ref("ARR"),
5022 AssignmentValue::Compound(ArrayExpr {
5023 kind: ArrayKind::Indexed,
5024 elements: vec![
5025 ArrayElem::Sequential(Word::literal("a").into()),
5026 ArrayElem::Sequential(Word::literal("b").into()),
5027 ArrayElem::Sequential(Word::literal("c").into()),
5028 ],
5029 span: Span::new(),
5030 }),
5031 );
5032 if let AssignmentValue::Compound(array) = &a.value {
5033 assert_eq!(array.elements.len(), 3);
5034 } else {
5035 panic!("expected Compound");
5036 }
5037 }
5038
5039 #[test]
5040 fn assignment_append() {
5041 let mut a = assignment(
5042 plain_ref("PATH"),
5043 AssignmentValue::Scalar(Word::literal("/usr/bin")),
5044 );
5045 a.append = true;
5046 assert!(a.append);
5047 }
5048
5049 #[test]
5050 fn assignment_indexed() {
5051 let a = assignment(
5052 indexed_ref("arr", "0"),
5053 AssignmentValue::Scalar(Word::literal("val")),
5054 );
5055 assert_eq!(
5056 a.target
5057 .subscript
5058 .as_ref()
5059 .map(|subscript| subscript.syntax_text("")),
5060 Some("0")
5061 );
5062 }
5063
5064 #[test]
5067 fn case_terminator_equality() {
5068 assert_eq!(CaseTerminator::Break, CaseTerminator::Break);
5069 assert_eq!(CaseTerminator::FallThrough, CaseTerminator::FallThrough);
5070 assert_eq!(CaseTerminator::Continue, CaseTerminator::Continue);
5071 assert_eq!(
5072 CaseTerminator::ContinueMatching,
5073 CaseTerminator::ContinueMatching
5074 );
5075 assert_ne!(CaseTerminator::Break, CaseTerminator::FallThrough);
5076 }
5077
5078 #[test]
5081 fn if_command_construction() {
5082 let if_cmd = IfCommand {
5083 condition: stmt_seq(vec![]),
5084 then_branch: stmt_seq(vec![]),
5085 elif_branches: vec![],
5086 else_branch: None,
5087 syntax: IfSyntax::ThenFi {
5088 then_span: Span::new(),
5089 fi_span: Span::new(),
5090 },
5091 span: Span::new(),
5092 };
5093 assert!(if_cmd.else_branch.is_none());
5094 assert!(if_cmd.elif_branches.is_empty());
5095 }
5096
5097 #[test]
5098 fn for_command_without_words() {
5099 let for_cmd = ForCommand {
5100 targets: vec![ForTarget {
5101 word: Word::literal("i"),
5102 name: Some("i".into()),
5103 span: Span::new(),
5104 }],
5105 words: None,
5106 body: stmt_seq(vec![]),
5107 syntax: ForSyntax::InDoDone {
5108 in_span: None,
5109 do_span: Span::new(),
5110 done_span: Span::new(),
5111 },
5112 span: Span::new(),
5113 };
5114 assert!(for_cmd.words.is_none());
5115 assert_eq!(for_cmd.targets[0].word.render(""), "i");
5116 assert_eq!(for_cmd.targets[0].name.as_deref(), Some("i"));
5117 }
5118
5119 #[test]
5120 fn for_command_with_words() {
5121 let for_cmd = ForCommand {
5122 targets: vec![ForTarget {
5123 word: Word::literal("x"),
5124 name: Some("x".into()),
5125 span: Span::new(),
5126 }],
5127 words: Some(vec![Word::literal("1"), Word::literal("2")]),
5128 body: stmt_seq(vec![]),
5129 syntax: ForSyntax::InDoDone {
5130 in_span: Some(Span::new()),
5131 do_span: Span::new(),
5132 done_span: Span::new(),
5133 },
5134 span: Span::new(),
5135 };
5136 assert_eq!(for_cmd.words.as_ref().unwrap().len(), 2);
5137 }
5138
5139 #[test]
5140 fn arithmetic_for_command() {
5141 let cmd = ArithmeticForCommand {
5142 left_paren_span: Span::new(),
5143 init_span: Some(Span::new()),
5144 init_ast: None,
5145 first_semicolon_span: Span::new(),
5146 condition_span: Some(Span::new()),
5147 condition_ast: None,
5148 second_semicolon_span: Span::new(),
5149 step_span: Some(Span::new()),
5150 step_ast: None,
5151 right_paren_span: Span::new(),
5152 body: stmt_seq(vec![]),
5153 span: Span::new(),
5154 };
5155 assert!(cmd.init_span.is_some());
5156 assert!(cmd.condition_span.is_some());
5157 assert!(cmd.step_span.is_some());
5158 }
5159
5160 #[test]
5161 fn function_def_construction() {
5162 let func = FunctionDef {
5163 header: FunctionHeader {
5164 function_keyword_span: None,
5165 entries: vec![FunctionHeaderEntry {
5166 word: Word::literal("my_func"),
5167 static_name: Some("my_func".into()),
5168 }],
5169 trailing_parens_span: Some(Span::new()),
5170 },
5171 body: Box::new(simple_stmt("echo", vec![Word::literal("hello")])),
5172 span: Span::new(),
5173 };
5174 assert_eq!(func.static_names().next().unwrap(), "my_func");
5175 }
5176
5177 #[test]
5180 fn file_empty() {
5181 let file = File {
5182 body: stmt_seq(vec![]),
5183 span: Span::new(),
5184 };
5185 assert!(file.body.is_empty());
5186 }
5187
5188 #[test]
5191 fn command_variants_constructible() {
5192 let simple = Command::Simple(simple_command("echo", vec![]));
5193 assert!(matches!(simple, Command::Simple(_)));
5194
5195 let pipe = Command::Binary(BinaryCommand {
5196 left: Box::new(simple_stmt("echo", vec![])),
5197 op: BinaryOp::Pipe,
5198 op_span: Span::new(),
5199 right: Box::new(simple_stmt("cat", vec![])),
5200 span: Span::new(),
5201 });
5202 assert!(matches!(pipe, Command::Binary(_)));
5203
5204 let builtin = Command::Builtin(BuiltinCommand::Exit(ExitCommand {
5205 code: Some(Word::literal("1")),
5206 extra_args: vec![],
5207 assignments: Box::default(),
5208 span: Span::new(),
5209 }));
5210 assert!(matches!(builtin, Command::Builtin(_)));
5211
5212 let compound = Command::Compound(CompoundCommand::BraceGroup(stmt_seq(vec![])));
5213 assert!(matches!(compound, Command::Compound(_)));
5214
5215 let func = Command::Function(FunctionDef {
5216 header: FunctionHeader {
5217 function_keyword_span: None,
5218 entries: vec![FunctionHeaderEntry {
5219 word: Word::literal("f"),
5220 static_name: Some("f".into()),
5221 }],
5222 trailing_parens_span: Some(Span::new()),
5223 },
5224 body: Box::new(simple_stmt("true", vec![])),
5225 span: Span::new(),
5226 });
5227 assert!(matches!(func, Command::Function(_)));
5228
5229 let anonymous = Command::AnonymousFunction(AnonymousFunctionCommand {
5230 surface: AnonymousFunctionSurface::Parens {
5231 parens_span: Span::new(),
5232 },
5233 body: Box::new(simple_stmt("true", vec![])),
5234 args: vec![Word::literal("x")],
5235 span: Span::new(),
5236 });
5237 assert!(matches!(anonymous, Command::AnonymousFunction(_)));
5238 }
5239
5240 #[test]
5243 fn compound_command_subshell() {
5244 let cmd = CompoundCommand::Subshell(stmt_seq(vec![]));
5245 assert!(matches!(cmd, CompoundCommand::Subshell(_)));
5246 }
5247
5248 #[test]
5249 fn compound_command_arithmetic() {
5250 let cmd = CompoundCommand::Arithmetic(ArithmeticCommand {
5251 span: Span::new(),
5252 left_paren_span: Span::new(),
5253 expr_span: Some(Span::new()),
5254 expr_ast: None,
5255 right_paren_span: Span::new(),
5256 });
5257 assert!(matches!(cmd, CompoundCommand::Arithmetic(_)));
5258 }
5259
5260 #[test]
5261 fn compound_command_conditional() {
5262 let cmd = CompoundCommand::Conditional(ConditionalCommand {
5263 expression: ConditionalExpr::Unary(ConditionalUnaryExpr {
5264 op: ConditionalUnaryOp::RegularFile,
5265 op_span: Span::new(),
5266 expr: Box::new(ConditionalExpr::Word(Word::literal("file"))),
5267 }),
5268 span: Span::new(),
5269 left_bracket_span: Span::new(),
5270 right_bracket_span: Span::new(),
5271 });
5272 if let CompoundCommand::Conditional(command) = &cmd {
5273 let ConditionalExpr::Unary(expr) = &command.expression else {
5274 panic!("expected unary conditional");
5275 };
5276 assert_eq!(expr.op, ConditionalUnaryOp::RegularFile);
5277 } else {
5278 panic!("expected Conditional");
5279 }
5280 }
5281
5282 #[test]
5283 fn time_command_construction() {
5284 let cmd = TimeCommand {
5285 posix_format: true,
5286 command: None,
5287 span: Span::new(),
5288 };
5289 assert!(cmd.posix_format);
5290 assert!(cmd.command.is_none());
5291 }
5292}