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.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- 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
|
+
]
|