just-bash 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. just_bash-0.1.5.dist-info/WHEEL +4 -0
just_bash/ast/types.py ADDED
@@ -0,0 +1,953 @@
1
+ """
2
+ Abstract Syntax Tree (AST) Types for Bash
3
+
4
+ This module defines the complete AST structure for bash scripts.
5
+ The design follows the actual bash grammar while being Pythonic.
6
+
7
+ Architecture:
8
+ Input → Lexer → Parser → AST → Expander → Interpreter → Output
9
+
10
+ Each node type corresponds to a bash construct and can be visited
11
+ by the tree-walking interpreter.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from typing import Literal, Union, Optional
18
+
19
+ # =============================================================================
20
+ # BASE TYPES
21
+ # =============================================================================
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Position:
26
+ """Position information for error reporting."""
27
+
28
+ line: int
29
+ column: int
30
+ offset: int
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Span:
35
+ """Span in source code."""
36
+
37
+ start: Position
38
+ end: Position
39
+
40
+
41
+ # =============================================================================
42
+ # SCRIPT & STATEMENTS
43
+ # =============================================================================
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ScriptNode:
48
+ """Root node: a complete script."""
49
+
50
+ type: Literal["Script"] = field(default="Script", repr=False)
51
+ statements: tuple["StatementNode", ...] = ()
52
+ line: Optional[int] = None
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class StatementNode:
57
+ """A statement is a list of pipelines connected by && or ||."""
58
+
59
+ type: Literal["Statement"] = field(default="Statement", repr=False)
60
+ pipelines: tuple["PipelineNode", ...] = ()
61
+ operators: tuple[Literal["&&", "||", ";"], ...] = ()
62
+ background: bool = False
63
+ line: Optional[int] = None
64
+
65
+
66
+ # =============================================================================
67
+ # PIPELINES & COMMANDS
68
+ # =============================================================================
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class PipelineNode:
73
+ """A pipeline: cmd1 | cmd2 | cmd3."""
74
+
75
+ type: Literal["Pipeline"] = field(default="Pipeline", repr=False)
76
+ commands: tuple["CommandNode", ...] = ()
77
+ negated: bool = False
78
+ line: Optional[int] = None
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class SimpleCommandNode:
83
+ """Simple command: name args... with optional redirections."""
84
+
85
+ type: Literal["SimpleCommand"] = field(default="SimpleCommand", repr=False)
86
+ assignments: tuple["AssignmentNode", ...] = ()
87
+ name: Optional["WordNode"] = None
88
+ args: tuple["WordNode", ...] = ()
89
+ redirections: tuple["RedirectionNode", ...] = ()
90
+ line: Optional[int] = None
91
+
92
+
93
+ # Forward declarations for compound commands
94
+ @dataclass(frozen=True)
95
+ class IfNode:
96
+ """if statement."""
97
+
98
+ type: Literal["If"] = field(default="If", repr=False)
99
+ clauses: tuple["IfClause", ...] = ()
100
+ else_body: Optional[tuple["StatementNode", ...]] = None
101
+ redirections: tuple["RedirectionNode", ...] = ()
102
+ line: Optional[int] = None
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class IfClause:
107
+ """A single if/elif clause."""
108
+
109
+ condition: tuple["StatementNode", ...] = ()
110
+ body: tuple["StatementNode", ...] = ()
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class ForNode:
115
+ """for loop: for VAR in WORDS; do ...; done."""
116
+
117
+ type: Literal["For"] = field(default="For", repr=False)
118
+ variable: str = ""
119
+ words: Optional[tuple["WordNode", ...]] = None # None = "$@"
120
+ body: tuple["StatementNode", ...] = ()
121
+ redirections: tuple["RedirectionNode", ...] = ()
122
+ line: Optional[int] = None
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class CStyleForNode:
127
+ """C-style for loop: for ((init; cond; step)); do ...; done."""
128
+
129
+ type: Literal["CStyleFor"] = field(default="CStyleFor", repr=False)
130
+ init: Optional["ArithmeticExpressionNode"] = None
131
+ condition: Optional["ArithmeticExpressionNode"] = None
132
+ update: Optional["ArithmeticExpressionNode"] = None
133
+ body: tuple["StatementNode", ...] = ()
134
+ redirections: tuple["RedirectionNode", ...] = ()
135
+ line: Optional[int] = None
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class WhileNode:
140
+ """while loop."""
141
+
142
+ type: Literal["While"] = field(default="While", repr=False)
143
+ condition: tuple["StatementNode", ...] = ()
144
+ body: tuple["StatementNode", ...] = ()
145
+ redirections: tuple["RedirectionNode", ...] = ()
146
+ line: Optional[int] = None
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class UntilNode:
151
+ """until loop."""
152
+
153
+ type: Literal["Until"] = field(default="Until", repr=False)
154
+ condition: tuple["StatementNode", ...] = ()
155
+ body: tuple["StatementNode", ...] = ()
156
+ redirections: tuple["RedirectionNode", ...] = ()
157
+ line: Optional[int] = None
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class CaseNode:
162
+ """case statement."""
163
+
164
+ type: Literal["Case"] = field(default="Case", repr=False)
165
+ word: Optional["WordNode"] = None
166
+ items: tuple["CaseItemNode", ...] = ()
167
+ redirections: tuple["RedirectionNode", ...] = ()
168
+ line: Optional[int] = None
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class CaseItemNode:
173
+ """A single case item with patterns and body."""
174
+
175
+ type: Literal["CaseItem"] = field(default="CaseItem", repr=False)
176
+ patterns: tuple["WordNode", ...] = ()
177
+ body: tuple["StatementNode", ...] = ()
178
+ terminator: Literal[";;", ";&", ";;&"] = ";;"
179
+ line: Optional[int] = None
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class SubshellNode:
184
+ """Subshell: ( ... )."""
185
+
186
+ type: Literal["Subshell"] = field(default="Subshell", repr=False)
187
+ body: tuple["StatementNode", ...] = ()
188
+ redirections: tuple["RedirectionNode", ...] = ()
189
+ line: Optional[int] = None
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class GroupNode:
194
+ """Command group: { ...; }."""
195
+
196
+ type: Literal["Group"] = field(default="Group", repr=False)
197
+ body: tuple["StatementNode", ...] = ()
198
+ redirections: tuple["RedirectionNode", ...] = ()
199
+ line: Optional[int] = None
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class ArithmeticCommandNode:
204
+ """Arithmetic command: (( expr ))."""
205
+
206
+ type: Literal["ArithmeticCommand"] = field(default="ArithmeticCommand", repr=False)
207
+ expression: Optional["ArithmeticExpressionNode"] = None
208
+ redirections: tuple["RedirectionNode", ...] = ()
209
+ line: Optional[int] = None
210
+
211
+
212
+ @dataclass(frozen=True)
213
+ class ConditionalCommandNode:
214
+ """Conditional command: [[ expr ]]."""
215
+
216
+ type: Literal["ConditionalCommand"] = field(default="ConditionalCommand", repr=False)
217
+ expression: Optional["ConditionalExpressionNode"] = None
218
+ redirections: tuple["RedirectionNode", ...] = ()
219
+ line: Optional[int] = None
220
+
221
+
222
+ # Union of all compound commands
223
+ CompoundCommandNode = Union[
224
+ IfNode,
225
+ ForNode,
226
+ CStyleForNode,
227
+ WhileNode,
228
+ UntilNode,
229
+ CaseNode,
230
+ SubshellNode,
231
+ GroupNode,
232
+ ArithmeticCommandNode,
233
+ ConditionalCommandNode,
234
+ ]
235
+
236
+
237
+ # =============================================================================
238
+ # FUNCTIONS
239
+ # =============================================================================
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class FunctionDefNode:
244
+ """Function definition."""
245
+
246
+ type: Literal["FunctionDef"] = field(default="FunctionDef", repr=False)
247
+ name: str = ""
248
+ body: Optional[CompoundCommandNode] = None
249
+ redirections: tuple["RedirectionNode", ...] = ()
250
+ line: Optional[int] = None
251
+
252
+
253
+ # Union of all command types
254
+ CommandNode = Union[SimpleCommandNode, CompoundCommandNode, FunctionDefNode]
255
+
256
+
257
+ # =============================================================================
258
+ # ASSIGNMENTS
259
+ # =============================================================================
260
+
261
+
262
+ @dataclass(frozen=True)
263
+ class AssignmentNode:
264
+ """Variable assignment: VAR=value or VAR+=value."""
265
+
266
+ type: Literal["Assignment"] = field(default="Assignment", repr=False)
267
+ name: str = ""
268
+ value: Optional["WordNode"] = None
269
+ append: bool = False
270
+ array: Optional[tuple["WordNode", ...]] = None
271
+ line: Optional[int] = None
272
+
273
+
274
+ # =============================================================================
275
+ # REDIRECTIONS
276
+ # =============================================================================
277
+
278
+ RedirectionOperator = Literal[
279
+ "<", # Input
280
+ ">", # Output (truncate)
281
+ ">>", # Output (append)
282
+ ">&", # Duplicate output fd
283
+ "<&", # Duplicate input fd
284
+ "<>", # Open for read/write
285
+ ">|", # Output (clobber)
286
+ "&>", # Redirect stdout and stderr
287
+ "&>>", # Append stdout and stderr
288
+ "<<<", # Here-string
289
+ "<<", # Here-document
290
+ "<<-", # Here-document (strip tabs)
291
+ ]
292
+
293
+
294
+ @dataclass(frozen=True)
295
+ class HereDocNode:
296
+ """Here document."""
297
+
298
+ type: Literal["HereDoc"] = field(default="HereDoc", repr=False)
299
+ delimiter: str = ""
300
+ content: Optional["WordNode"] = None
301
+ strip_tabs: bool = False
302
+ quoted: bool = False
303
+ line: Optional[int] = None
304
+
305
+
306
+ @dataclass(frozen=True)
307
+ class RedirectionNode:
308
+ """I/O redirection."""
309
+
310
+ type: Literal["Redirection"] = field(default="Redirection", repr=False)
311
+ fd: Optional[int] = None
312
+ operator: RedirectionOperator = "<"
313
+ target: Union["WordNode", HereDocNode, None] = None
314
+ line: Optional[int] = None
315
+
316
+
317
+ # =============================================================================
318
+ # WORDS (the heart of shell parsing)
319
+ # =============================================================================
320
+
321
+
322
+ @dataclass(frozen=True)
323
+ class LiteralPart:
324
+ """Literal text (no special meaning)."""
325
+
326
+ type: Literal["Literal"] = field(default="Literal", repr=False)
327
+ value: str = ""
328
+ line: Optional[int] = None
329
+
330
+
331
+ @dataclass(frozen=True)
332
+ class SingleQuotedPart:
333
+ """Single-quoted string: 'literal'."""
334
+
335
+ type: Literal["SingleQuoted"] = field(default="SingleQuoted", repr=False)
336
+ value: str = ""
337
+ line: Optional[int] = None
338
+
339
+
340
+ @dataclass(frozen=True)
341
+ class EscapedPart:
342
+ """Escaped character: \\x."""
343
+
344
+ type: Literal["Escaped"] = field(default="Escaped", repr=False)
345
+ value: str = ""
346
+ line: Optional[int] = None
347
+
348
+
349
+ @dataclass(frozen=True)
350
+ class GlobPart:
351
+ """Glob pattern part (expanded during pathname expansion)."""
352
+
353
+ type: Literal["Glob"] = field(default="Glob", repr=False)
354
+ pattern: str = ""
355
+ line: Optional[int] = None
356
+
357
+
358
+ @dataclass(frozen=True)
359
+ class TildeExpansionPart:
360
+ """Tilde expansion: ~ or ~user."""
361
+
362
+ type: Literal["TildeExpansion"] = field(default="TildeExpansion", repr=False)
363
+ user: Optional[str] = None # None = current user
364
+ line: Optional[int] = None
365
+
366
+
367
+ # =============================================================================
368
+ # PARAMETER EXPANSION
369
+ # =============================================================================
370
+
371
+
372
+ @dataclass(frozen=True)
373
+ class DefaultValueOp:
374
+ """${VAR:-default} or ${VAR-default}."""
375
+
376
+ type: Literal["DefaultValue"] = field(default="DefaultValue", repr=False)
377
+ word: Optional["WordNode"] = None
378
+ check_empty: bool = False # : present = check empty too
379
+
380
+
381
+ @dataclass(frozen=True)
382
+ class AssignDefaultOp:
383
+ """${VAR:=default} or ${VAR=default}."""
384
+
385
+ type: Literal["AssignDefault"] = field(default="AssignDefault", repr=False)
386
+ word: Optional["WordNode"] = None
387
+ check_empty: bool = False
388
+
389
+
390
+ @dataclass(frozen=True)
391
+ class ErrorIfUnsetOp:
392
+ """${VAR:?error} or ${VAR?error}."""
393
+
394
+ type: Literal["ErrorIfUnset"] = field(default="ErrorIfUnset", repr=False)
395
+ word: Optional["WordNode"] = None
396
+ check_empty: bool = False
397
+
398
+
399
+ @dataclass(frozen=True)
400
+ class UseAlternativeOp:
401
+ """${VAR:+alternative} or ${VAR+alternative}."""
402
+
403
+ type: Literal["UseAlternative"] = field(default="UseAlternative", repr=False)
404
+ word: Optional["WordNode"] = None
405
+ check_empty: bool = False
406
+
407
+
408
+ @dataclass(frozen=True)
409
+ class LengthOp:
410
+ """${#VAR}."""
411
+
412
+ type: Literal["Length"] = field(default="Length", repr=False)
413
+
414
+
415
+ @dataclass(frozen=True)
416
+ class LengthSliceErrorOp:
417
+ """${#VAR:...} - invalid syntax, length cannot have substring."""
418
+
419
+ type: Literal["LengthSliceError"] = field(default="LengthSliceError", repr=False)
420
+
421
+
422
+ @dataclass(frozen=True)
423
+ class SubstringOp:
424
+ """${VAR:offset} or ${VAR:offset:length}."""
425
+
426
+ type: Literal["Substring"] = field(default="Substring", repr=False)
427
+ offset: Optional["ArithmeticExpressionNode"] = None
428
+ length: Optional["ArithmeticExpressionNode"] = None
429
+
430
+
431
+ @dataclass(frozen=True)
432
+ class PatternRemovalOp:
433
+ """${VAR#pattern}, ${VAR##pattern}, ${VAR%pattern}, ${VAR%%pattern}."""
434
+
435
+ type: Literal["PatternRemoval"] = field(default="PatternRemoval", repr=False)
436
+ pattern: Optional["WordNode"] = None
437
+ side: Literal["prefix", "suffix"] = "prefix"
438
+ greedy: bool = False
439
+
440
+
441
+ @dataclass(frozen=True)
442
+ class PatternReplacementOp:
443
+ """${VAR/pattern/replacement} or ${VAR//pattern/replacement}."""
444
+
445
+ type: Literal["PatternReplacement"] = field(default="PatternReplacement", repr=False)
446
+ pattern: Optional["WordNode"] = None
447
+ replacement: Optional["WordNode"] = None
448
+ all: bool = False
449
+ anchor: Optional[Literal["start", "end"]] = None
450
+
451
+
452
+ @dataclass(frozen=True)
453
+ class CaseModificationOp:
454
+ """${VAR^}, ${VAR^^}, ${VAR,}, ${VAR,,}."""
455
+
456
+ type: Literal["CaseModification"] = field(default="CaseModification", repr=False)
457
+ direction: Literal["upper", "lower"] = "upper"
458
+ all: bool = False
459
+ pattern: Optional["WordNode"] = None
460
+
461
+
462
+ @dataclass(frozen=True)
463
+ class TransformOp:
464
+ """${var@Q}, ${var@P}, etc. - parameter transformation."""
465
+
466
+ type: Literal["Transform"] = field(default="Transform", repr=False)
467
+ operator: Literal["Q", "P", "a", "A", "E", "K"] = "Q"
468
+
469
+
470
+ @dataclass(frozen=True)
471
+ class IndirectionOp:
472
+ """${!VAR} - indirect expansion."""
473
+
474
+ type: Literal["Indirection"] = field(default="Indirection", repr=False)
475
+
476
+
477
+ @dataclass(frozen=True)
478
+ class ArrayKeysOp:
479
+ """${!arr[@]} or ${!arr[*]} - array keys/indices."""
480
+
481
+ type: Literal["ArrayKeys"] = field(default="ArrayKeys", repr=False)
482
+ array: str = ""
483
+ star: bool = False # True if [*] was used instead of [@]
484
+
485
+
486
+ @dataclass(frozen=True)
487
+ class VarNamePrefixOp:
488
+ """${!prefix*} or ${!prefix@} - list variable names with prefix."""
489
+
490
+ type: Literal["VarNamePrefix"] = field(default="VarNamePrefix", repr=False)
491
+ prefix: str = ""
492
+ star: bool = False # True if * was used instead of @
493
+
494
+
495
+ # Union of all parameter operations
496
+ ParameterOperation = Union[
497
+ DefaultValueOp,
498
+ AssignDefaultOp,
499
+ ErrorIfUnsetOp,
500
+ UseAlternativeOp,
501
+ LengthOp,
502
+ LengthSliceErrorOp,
503
+ SubstringOp,
504
+ PatternRemovalOp,
505
+ PatternReplacementOp,
506
+ CaseModificationOp,
507
+ TransformOp,
508
+ IndirectionOp,
509
+ ArrayKeysOp,
510
+ VarNamePrefixOp,
511
+ ]
512
+
513
+
514
+ @dataclass(frozen=True)
515
+ class ParameterExpansionPart:
516
+ """Parameter/variable expansion: $VAR or ${VAR...}."""
517
+
518
+ type: Literal["ParameterExpansion"] = field(default="ParameterExpansion", repr=False)
519
+ parameter: str = ""
520
+ operation: Optional[ParameterOperation] = None
521
+ line: Optional[int] = None
522
+
523
+
524
+ # =============================================================================
525
+ # COMMAND SUBSTITUTION
526
+ # =============================================================================
527
+
528
+
529
+ @dataclass(frozen=True)
530
+ class CommandSubstitutionPart:
531
+ """Command substitution: $(cmd) or `cmd`."""
532
+
533
+ type: Literal["CommandSubstitution"] = field(default="CommandSubstitution", repr=False)
534
+ body: Optional[ScriptNode] = None
535
+ legacy: bool = False # True for backtick syntax
536
+ line: Optional[int] = None
537
+
538
+
539
+ # =============================================================================
540
+ # ARITHMETIC
541
+ # =============================================================================
542
+
543
+
544
+ @dataclass(frozen=True)
545
+ class ArithNumberNode:
546
+ """Numeric literal."""
547
+
548
+ type: Literal["ArithNumber"] = field(default="ArithNumber", repr=False)
549
+ value: int = 0
550
+ line: Optional[int] = None
551
+
552
+
553
+ @dataclass(frozen=True)
554
+ class ArithVariableNode:
555
+ """Variable reference in arithmetic context."""
556
+
557
+ type: Literal["ArithVariable"] = field(default="ArithVariable", repr=False)
558
+ name: str = ""
559
+ line: Optional[int] = None
560
+
561
+
562
+ ArithBinaryOperator = Literal[
563
+ "+", "-", "*", "/", "%", "**",
564
+ "<<", ">>",
565
+ "<", "<=", ">", ">=", "==", "!=",
566
+ "&", "|", "^",
567
+ "&&", "||",
568
+ ",",
569
+ ]
570
+
571
+
572
+ @dataclass(frozen=True)
573
+ class ArithBinaryNode:
574
+ """Binary arithmetic operation."""
575
+
576
+ type: Literal["ArithBinary"] = field(default="ArithBinary", repr=False)
577
+ operator: ArithBinaryOperator = "+"
578
+ left: Optional["ArithExpr"] = None
579
+ right: Optional["ArithExpr"] = None
580
+ line: Optional[int] = None
581
+
582
+
583
+ ArithUnaryOperator = Literal["-", "+", "!", "~", "++", "--"]
584
+
585
+
586
+ @dataclass(frozen=True)
587
+ class ArithUnaryNode:
588
+ """Unary arithmetic operation."""
589
+
590
+ type: Literal["ArithUnary"] = field(default="ArithUnary", repr=False)
591
+ operator: ArithUnaryOperator = "-"
592
+ operand: Optional["ArithExpr"] = None
593
+ prefix: bool = True
594
+ line: Optional[int] = None
595
+
596
+
597
+ @dataclass(frozen=True)
598
+ class ArithTernaryNode:
599
+ """Ternary conditional: cond ? a : b."""
600
+
601
+ type: Literal["ArithTernary"] = field(default="ArithTernary", repr=False)
602
+ condition: Optional["ArithExpr"] = None
603
+ consequent: Optional["ArithExpr"] = None
604
+ alternate: Optional["ArithExpr"] = None
605
+ line: Optional[int] = None
606
+
607
+
608
+ ArithAssignmentOperator = Literal[
609
+ "=", "+=", "-=", "*=", "/=", "%=",
610
+ "<<=", ">>=", "&=", "|=", "^=",
611
+ ]
612
+
613
+
614
+ @dataclass(frozen=True)
615
+ class ArithAssignmentNode:
616
+ """Arithmetic assignment."""
617
+
618
+ type: Literal["ArithAssignment"] = field(default="ArithAssignment", repr=False)
619
+ operator: ArithAssignmentOperator = "="
620
+ variable: str = ""
621
+ subscript: Optional["ArithExpr"] = None
622
+ string_key: Optional[str] = None # For associative arrays
623
+ value: Optional["ArithExpr"] = None
624
+ line: Optional[int] = None
625
+
626
+
627
+ @dataclass(frozen=True)
628
+ class ArithGroupNode:
629
+ """Parenthesized arithmetic expression."""
630
+
631
+ type: Literal["ArithGroup"] = field(default="ArithGroup", repr=False)
632
+ expression: Optional["ArithExpr"] = None
633
+ line: Optional[int] = None
634
+
635
+
636
+ @dataclass(frozen=True)
637
+ class ArithNestedNode:
638
+ """Nested arithmetic expansion within arithmetic context: $((expr))."""
639
+
640
+ type: Literal["ArithNested"] = field(default="ArithNested", repr=False)
641
+ expression: Optional["ArithExpr"] = None
642
+ line: Optional[int] = None
643
+
644
+
645
+ @dataclass(frozen=True)
646
+ class ArithCommandSubstNode:
647
+ """Command substitution within arithmetic context: $(cmd) or `cmd`."""
648
+
649
+ type: Literal["ArithCommandSubst"] = field(default="ArithCommandSubst", repr=False)
650
+ command: str = ""
651
+ line: Optional[int] = None
652
+
653
+
654
+ @dataclass(frozen=True)
655
+ class ArithBracedExpansionNode:
656
+ """Braced expansion in arithmetic: ${...}."""
657
+
658
+ type: Literal["ArithBracedExpansion"] = field(default="ArithBracedExpansion", repr=False)
659
+ content: str = ""
660
+ line: Optional[int] = None
661
+
662
+
663
+ @dataclass(frozen=True)
664
+ class ArithDynamicBaseNode:
665
+ """Dynamic base constant: ${base}#value."""
666
+
667
+ type: Literal["ArithDynamicBase"] = field(default="ArithDynamicBase", repr=False)
668
+ base_expr: str = ""
669
+ value: str = ""
670
+ line: Optional[int] = None
671
+
672
+
673
+ @dataclass(frozen=True)
674
+ class ArithDynamicNumberNode:
675
+ """Dynamic number prefix: ${zero}11 or ${zero}xAB."""
676
+
677
+ type: Literal["ArithDynamicNumber"] = field(default="ArithDynamicNumber", repr=False)
678
+ prefix: str = ""
679
+ suffix: str = ""
680
+ line: Optional[int] = None
681
+
682
+
683
+ @dataclass(frozen=True)
684
+ class ArithConcatNode:
685
+ """Concatenation of parts forming a single numeric value."""
686
+
687
+ type: Literal["ArithConcat"] = field(default="ArithConcat", repr=False)
688
+ parts: tuple["ArithExpr", ...] = ()
689
+ line: Optional[int] = None
690
+
691
+
692
+ @dataclass(frozen=True)
693
+ class ArithArrayElementNode:
694
+ """Array element access in arithmetic context."""
695
+
696
+ type: Literal["ArithArrayElement"] = field(default="ArithArrayElement", repr=False)
697
+ array: str = ""
698
+ index: Optional["ArithExpr"] = None
699
+ string_key: Optional[str] = None # For associative arrays
700
+ line: Optional[int] = None
701
+
702
+
703
+ @dataclass(frozen=True)
704
+ class ArithDoubleSubscriptNode:
705
+ """Invalid double subscript node (e.g., a[1][1])."""
706
+
707
+ type: Literal["ArithDoubleSubscript"] = field(default="ArithDoubleSubscript", repr=False)
708
+ array: str = ""
709
+ index: Optional["ArithExpr"] = None
710
+ line: Optional[int] = None
711
+
712
+
713
+ @dataclass(frozen=True)
714
+ class ArithNumberSubscriptNode:
715
+ """Invalid number subscript node (e.g., 1[2])."""
716
+
717
+ type: Literal["ArithNumberSubscript"] = field(default="ArithNumberSubscript", repr=False)
718
+ number: str = ""
719
+ error_token: str = ""
720
+ line: Optional[int] = None
721
+
722
+
723
+ # Union of all arithmetic expressions
724
+ ArithExpr = Union[
725
+ ArithNumberNode,
726
+ ArithVariableNode,
727
+ ArithBinaryNode,
728
+ ArithUnaryNode,
729
+ ArithTernaryNode,
730
+ ArithAssignmentNode,
731
+ ArithGroupNode,
732
+ ArithNestedNode,
733
+ ArithCommandSubstNode,
734
+ ArithBracedExpansionNode,
735
+ ArithDynamicBaseNode,
736
+ ArithDynamicNumberNode,
737
+ ArithConcatNode,
738
+ ArithArrayElementNode,
739
+ ArithDoubleSubscriptNode,
740
+ ArithNumberSubscriptNode,
741
+ ]
742
+
743
+
744
+ @dataclass(frozen=True)
745
+ class ArithmeticExpressionNode:
746
+ """Arithmetic expression wrapper (for $((...)) and ((...)))."""
747
+
748
+ type: Literal["ArithmeticExpression"] = field(default="ArithmeticExpression", repr=False)
749
+ expression: Optional[ArithExpr] = None
750
+ line: Optional[int] = None
751
+
752
+
753
+ @dataclass(frozen=True)
754
+ class ArithmeticExpansionPart:
755
+ """Arithmetic expansion: $((expr))."""
756
+
757
+ type: Literal["ArithmeticExpansion"] = field(default="ArithmeticExpansion", repr=False)
758
+ expression: Optional[ArithmeticExpressionNode] = None
759
+ line: Optional[int] = None
760
+
761
+
762
+ # =============================================================================
763
+ # PROCESS SUBSTITUTION
764
+ # =============================================================================
765
+
766
+
767
+ @dataclass(frozen=True)
768
+ class ProcessSubstitutionPart:
769
+ """Process substitution: <(cmd) or >(cmd)."""
770
+
771
+ type: Literal["ProcessSubstitution"] = field(default="ProcessSubstitution", repr=False)
772
+ body: Optional[ScriptNode] = None
773
+ direction: Literal["input", "output"] = "input" # <(...) vs >(...)
774
+ line: Optional[int] = None
775
+
776
+
777
+ # =============================================================================
778
+ # BRACE EXPANSION
779
+ # =============================================================================
780
+
781
+
782
+ @dataclass(frozen=True)
783
+ class BraceWordItem:
784
+ """A word item in brace expansion: {a,b,c}."""
785
+
786
+ type: Literal["Word"] = field(default="Word", repr=False)
787
+ word: Optional["WordNode"] = None
788
+
789
+
790
+ @dataclass(frozen=True)
791
+ class BraceRangeItem:
792
+ """A range item in brace expansion: {1..10}."""
793
+
794
+ type: Literal["Range"] = field(default="Range", repr=False)
795
+ start: Union[str, int] = ""
796
+ end: Union[str, int] = ""
797
+ step: Optional[int] = None
798
+ start_str: Optional[str] = None # For zero-padding
799
+ end_str: Optional[str] = None
800
+
801
+
802
+ BraceItem = Union[BraceWordItem, BraceRangeItem]
803
+
804
+
805
+ @dataclass(frozen=True)
806
+ class BraceExpansionPart:
807
+ """Brace expansion: {a,b,c} or {1..10}."""
808
+
809
+ type: Literal["BraceExpansion"] = field(default="BraceExpansion", repr=False)
810
+ items: tuple[BraceItem, ...] = ()
811
+ line: Optional[int] = None
812
+
813
+
814
+ # =============================================================================
815
+ # DOUBLE QUOTED (must be defined after all parts it can contain)
816
+ # =============================================================================
817
+
818
+
819
+ @dataclass(frozen=True)
820
+ class DoubleQuotedPart:
821
+ """Double-quoted string: "with $expansion"."""
822
+
823
+ type: Literal["DoubleQuoted"] = field(default="DoubleQuoted", repr=False)
824
+ parts: tuple["WordPart", ...] = ()
825
+ line: Optional[int] = None
826
+
827
+
828
+ # Union of all word parts
829
+ WordPart = Union[
830
+ LiteralPart,
831
+ SingleQuotedPart,
832
+ DoubleQuotedPart,
833
+ EscapedPart,
834
+ ParameterExpansionPart,
835
+ CommandSubstitutionPart,
836
+ ArithmeticExpansionPart,
837
+ ProcessSubstitutionPart,
838
+ BraceExpansionPart,
839
+ TildeExpansionPart,
840
+ GlobPart,
841
+ ]
842
+
843
+
844
+ @dataclass(frozen=True)
845
+ class WordNode:
846
+ """
847
+ A Word is a sequence of parts that form a single shell word.
848
+ After expansion, it may produce zero, one, or multiple strings.
849
+ """
850
+
851
+ type: Literal["Word"] = field(default="Word", repr=False)
852
+ parts: tuple[WordPart, ...] = ()
853
+ line: Optional[int] = None
854
+
855
+
856
+ # =============================================================================
857
+ # CONDITIONAL EXPRESSIONS (for [[ ]])
858
+ # =============================================================================
859
+
860
+ CondBinaryOperator = Literal[
861
+ "=", "==", "!=", "=~",
862
+ "<", ">",
863
+ "-eq", "-ne", "-lt", "-le", "-gt", "-ge",
864
+ "-nt", "-ot", "-ef",
865
+ ]
866
+
867
+
868
+ @dataclass(frozen=True)
869
+ class CondBinaryNode:
870
+ """Binary conditional expression."""
871
+
872
+ type: Literal["CondBinary"] = field(default="CondBinary", repr=False)
873
+ operator: CondBinaryOperator = "="
874
+ left: Optional[WordNode] = None
875
+ right: Optional[WordNode] = None
876
+ line: Optional[int] = None
877
+
878
+
879
+ CondUnaryOperator = Literal[
880
+ "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-k", "-p",
881
+ "-r", "-s", "-t", "-u", "-w", "-x",
882
+ "-G", "-L", "-N", "-O", "-S",
883
+ "-z", "-n", "-o", "-v", "-R",
884
+ ]
885
+
886
+
887
+ @dataclass(frozen=True)
888
+ class CondUnaryNode:
889
+ """Unary conditional expression."""
890
+
891
+ type: Literal["CondUnary"] = field(default="CondUnary", repr=False)
892
+ operator: CondUnaryOperator = "-e"
893
+ operand: Optional[WordNode] = None
894
+ line: Optional[int] = None
895
+
896
+
897
+ @dataclass(frozen=True)
898
+ class CondNotNode:
899
+ """Negated conditional expression."""
900
+
901
+ type: Literal["CondNot"] = field(default="CondNot", repr=False)
902
+ operand: Optional["ConditionalExpressionNode"] = None
903
+ line: Optional[int] = None
904
+
905
+
906
+ @dataclass(frozen=True)
907
+ class CondAndNode:
908
+ """AND conditional expression."""
909
+
910
+ type: Literal["CondAnd"] = field(default="CondAnd", repr=False)
911
+ left: Optional["ConditionalExpressionNode"] = None
912
+ right: Optional["ConditionalExpressionNode"] = None
913
+ line: Optional[int] = None
914
+
915
+
916
+ @dataclass(frozen=True)
917
+ class CondOrNode:
918
+ """OR conditional expression."""
919
+
920
+ type: Literal["CondOr"] = field(default="CondOr", repr=False)
921
+ left: Optional["ConditionalExpressionNode"] = None
922
+ right: Optional["ConditionalExpressionNode"] = None
923
+ line: Optional[int] = None
924
+
925
+
926
+ @dataclass(frozen=True)
927
+ class CondGroupNode:
928
+ """Grouped conditional expression."""
929
+
930
+ type: Literal["CondGroup"] = field(default="CondGroup", repr=False)
931
+ expression: Optional["ConditionalExpressionNode"] = None
932
+ line: Optional[int] = None
933
+
934
+
935
+ @dataclass(frozen=True)
936
+ class CondWordNode:
937
+ """Word in conditional context."""
938
+
939
+ type: Literal["CondWord"] = field(default="CondWord", repr=False)
940
+ word: Optional[WordNode] = None
941
+ line: Optional[int] = None
942
+
943
+
944
+ # Union of all conditional expressions
945
+ ConditionalExpressionNode = Union[
946
+ CondBinaryNode,
947
+ CondUnaryNode,
948
+ CondNotNode,
949
+ CondAndNode,
950
+ CondOrNode,
951
+ CondGroupNode,
952
+ CondWordNode,
953
+ ]