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