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
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""Parser for jq expressions.
|
|
2
|
+
|
|
3
|
+
Converts a token sequence into an AST using recursive descent parsing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .tokenizer import tokenize
|
|
7
|
+
from .types import (
|
|
8
|
+
ArrayNode,
|
|
9
|
+
AstNode,
|
|
10
|
+
BinaryOpNode,
|
|
11
|
+
CallNode,
|
|
12
|
+
CommaNode,
|
|
13
|
+
CondNode,
|
|
14
|
+
ElifBranch,
|
|
15
|
+
FieldNode,
|
|
16
|
+
ForeachNode,
|
|
17
|
+
IdentityNode,
|
|
18
|
+
IndexNode,
|
|
19
|
+
IterateNode,
|
|
20
|
+
LiteralNode,
|
|
21
|
+
ObjectEntry,
|
|
22
|
+
ObjectNode,
|
|
23
|
+
OptionalNode,
|
|
24
|
+
ParenNode,
|
|
25
|
+
PipeNode,
|
|
26
|
+
RecurseNode,
|
|
27
|
+
ReduceNode,
|
|
28
|
+
SliceNode,
|
|
29
|
+
StringInterpNode,
|
|
30
|
+
Token,
|
|
31
|
+
TokenType,
|
|
32
|
+
TryNode,
|
|
33
|
+
UnaryOpNode,
|
|
34
|
+
UpdateOpNode,
|
|
35
|
+
VarBindNode,
|
|
36
|
+
VarRefNode,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Parser:
|
|
41
|
+
"""Recursive descent parser for jq expressions."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, tokens: list[Token]):
|
|
44
|
+
self.tokens = tokens
|
|
45
|
+
self.pos = 0
|
|
46
|
+
|
|
47
|
+
def peek(self, offset: int = 0) -> Token:
|
|
48
|
+
"""Look at token at current position + offset."""
|
|
49
|
+
idx = self.pos + offset
|
|
50
|
+
if idx < len(self.tokens):
|
|
51
|
+
return self.tokens[idx]
|
|
52
|
+
return Token(TokenType.EOF, None, -1)
|
|
53
|
+
|
|
54
|
+
def advance(self) -> Token:
|
|
55
|
+
"""Advance and return current token."""
|
|
56
|
+
tok = (
|
|
57
|
+
self.tokens[self.pos] if self.pos < len(self.tokens) else Token(TokenType.EOF, None, -1)
|
|
58
|
+
)
|
|
59
|
+
self.pos += 1
|
|
60
|
+
return tok
|
|
61
|
+
|
|
62
|
+
def check(self, type_: TokenType) -> bool:
|
|
63
|
+
"""Check if current token is of given type."""
|
|
64
|
+
return self.peek().type == type_
|
|
65
|
+
|
|
66
|
+
def match(self, *types: TokenType) -> Token | None:
|
|
67
|
+
"""If current token matches any type, advance and return it."""
|
|
68
|
+
for t in types:
|
|
69
|
+
if self.check(t):
|
|
70
|
+
return self.advance()
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def expect(self, type_: TokenType, msg: str) -> Token:
|
|
74
|
+
"""Expect current token to be of given type, or raise error."""
|
|
75
|
+
if not self.check(type_):
|
|
76
|
+
raise ValueError(f"{msg} at position {self.peek().pos}, got {self.peek().type.name}")
|
|
77
|
+
return self.advance()
|
|
78
|
+
|
|
79
|
+
def parse(self) -> AstNode:
|
|
80
|
+
"""Parse the entire expression."""
|
|
81
|
+
expr = self.parse_expr()
|
|
82
|
+
if not self.check(TokenType.EOF):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Unexpected token {self.peek().type.name} at position {self.peek().pos}"
|
|
85
|
+
)
|
|
86
|
+
return expr
|
|
87
|
+
|
|
88
|
+
def parse_expr(self) -> AstNode:
|
|
89
|
+
"""Parse an expression (top level)."""
|
|
90
|
+
return self.parse_pipe()
|
|
91
|
+
|
|
92
|
+
def parse_pipe(self) -> AstNode:
|
|
93
|
+
"""Parse pipe expressions (left-associative |)."""
|
|
94
|
+
left = self.parse_comma()
|
|
95
|
+
while self.match(TokenType.PIPE):
|
|
96
|
+
right = self.parse_comma()
|
|
97
|
+
left = PipeNode(left, right)
|
|
98
|
+
return left
|
|
99
|
+
|
|
100
|
+
def parse_comma(self) -> AstNode:
|
|
101
|
+
"""Parse comma expressions (left-associative ,)."""
|
|
102
|
+
left = self.parse_var_bind()
|
|
103
|
+
while self.match(TokenType.COMMA):
|
|
104
|
+
right = self.parse_var_bind()
|
|
105
|
+
left = CommaNode(left, right)
|
|
106
|
+
return left
|
|
107
|
+
|
|
108
|
+
def parse_var_bind(self) -> AstNode:
|
|
109
|
+
"""Parse variable binding (expr as $var | body)."""
|
|
110
|
+
expr = self.parse_update()
|
|
111
|
+
if self.match(TokenType.AS):
|
|
112
|
+
var_token = self.expect(TokenType.IDENT, "Expected variable name after 'as'")
|
|
113
|
+
var_name = var_token.value
|
|
114
|
+
if not isinstance(var_name, str) or not var_name.startswith("$"):
|
|
115
|
+
raise ValueError(f"Variable name must start with $ at position {var_token.pos}")
|
|
116
|
+
self.expect(TokenType.PIPE, "Expected '|' after variable binding")
|
|
117
|
+
body = self.parse_expr()
|
|
118
|
+
return VarBindNode(var_name, expr, body)
|
|
119
|
+
return expr
|
|
120
|
+
|
|
121
|
+
def parse_update(self) -> AstNode:
|
|
122
|
+
"""Parse update operators (=, |=, +=, -=, *=, /=, %=, //=)."""
|
|
123
|
+
left = self.parse_alt()
|
|
124
|
+
op_map = {
|
|
125
|
+
TokenType.ASSIGN: "=",
|
|
126
|
+
TokenType.UPDATE_ADD: "+=",
|
|
127
|
+
TokenType.UPDATE_SUB: "-=",
|
|
128
|
+
TokenType.UPDATE_MUL: "*=",
|
|
129
|
+
TokenType.UPDATE_DIV: "/=",
|
|
130
|
+
TokenType.UPDATE_MOD: "%=",
|
|
131
|
+
TokenType.UPDATE_ALT: "//=",
|
|
132
|
+
TokenType.UPDATE_PIPE: "|=",
|
|
133
|
+
}
|
|
134
|
+
tok = self.match(
|
|
135
|
+
TokenType.ASSIGN,
|
|
136
|
+
TokenType.UPDATE_ADD,
|
|
137
|
+
TokenType.UPDATE_SUB,
|
|
138
|
+
TokenType.UPDATE_MUL,
|
|
139
|
+
TokenType.UPDATE_DIV,
|
|
140
|
+
TokenType.UPDATE_MOD,
|
|
141
|
+
TokenType.UPDATE_ALT,
|
|
142
|
+
TokenType.UPDATE_PIPE,
|
|
143
|
+
)
|
|
144
|
+
if tok:
|
|
145
|
+
value = self.parse_var_bind()
|
|
146
|
+
return UpdateOpNode(op_map[tok.type], left, value)
|
|
147
|
+
return left
|
|
148
|
+
|
|
149
|
+
def parse_alt(self) -> AstNode:
|
|
150
|
+
"""Parse alternative operator (//)."""
|
|
151
|
+
left = self.parse_or()
|
|
152
|
+
while self.match(TokenType.ALT):
|
|
153
|
+
right = self.parse_or()
|
|
154
|
+
left = BinaryOpNode("//", left, right)
|
|
155
|
+
return left
|
|
156
|
+
|
|
157
|
+
def parse_or(self) -> AstNode:
|
|
158
|
+
"""Parse or operator."""
|
|
159
|
+
left = self.parse_and()
|
|
160
|
+
while self.match(TokenType.OR):
|
|
161
|
+
right = self.parse_and()
|
|
162
|
+
left = BinaryOpNode("or", left, right)
|
|
163
|
+
return left
|
|
164
|
+
|
|
165
|
+
def parse_and(self) -> AstNode:
|
|
166
|
+
"""Parse and operator."""
|
|
167
|
+
left = self.parse_comparison()
|
|
168
|
+
while self.match(TokenType.AND):
|
|
169
|
+
right = self.parse_comparison()
|
|
170
|
+
left = BinaryOpNode("and", left, right)
|
|
171
|
+
return left
|
|
172
|
+
|
|
173
|
+
def parse_comparison(self) -> AstNode:
|
|
174
|
+
"""Parse comparison operators (==, !=, <, <=, >, >=)."""
|
|
175
|
+
left = self.parse_add_sub()
|
|
176
|
+
op_map = {
|
|
177
|
+
TokenType.EQ: "==",
|
|
178
|
+
TokenType.NE: "!=",
|
|
179
|
+
TokenType.LT: "<",
|
|
180
|
+
TokenType.LE: "<=",
|
|
181
|
+
TokenType.GT: ">",
|
|
182
|
+
TokenType.GE: ">=",
|
|
183
|
+
}
|
|
184
|
+
tok = self.match(
|
|
185
|
+
TokenType.EQ, TokenType.NE, TokenType.LT, TokenType.LE, TokenType.GT, TokenType.GE
|
|
186
|
+
)
|
|
187
|
+
if tok:
|
|
188
|
+
right = self.parse_add_sub()
|
|
189
|
+
left = BinaryOpNode(op_map[tok.type], left, right)
|
|
190
|
+
return left
|
|
191
|
+
|
|
192
|
+
def parse_add_sub(self) -> AstNode:
|
|
193
|
+
"""Parse addition and subtraction (left-associative)."""
|
|
194
|
+
left = self.parse_mul_div()
|
|
195
|
+
while True:
|
|
196
|
+
if self.match(TokenType.PLUS):
|
|
197
|
+
right = self.parse_mul_div()
|
|
198
|
+
left = BinaryOpNode("+", left, right)
|
|
199
|
+
elif self.match(TokenType.MINUS):
|
|
200
|
+
right = self.parse_mul_div()
|
|
201
|
+
left = BinaryOpNode("-", left, right)
|
|
202
|
+
else:
|
|
203
|
+
break
|
|
204
|
+
return left
|
|
205
|
+
|
|
206
|
+
def parse_mul_div(self) -> AstNode:
|
|
207
|
+
"""Parse multiplication, division, and modulo (left-associative)."""
|
|
208
|
+
left = self.parse_unary()
|
|
209
|
+
while True:
|
|
210
|
+
if self.match(TokenType.STAR):
|
|
211
|
+
right = self.parse_unary()
|
|
212
|
+
left = BinaryOpNode("*", left, right)
|
|
213
|
+
elif self.match(TokenType.SLASH):
|
|
214
|
+
right = self.parse_unary()
|
|
215
|
+
left = BinaryOpNode("/", left, right)
|
|
216
|
+
elif self.match(TokenType.PERCENT):
|
|
217
|
+
right = self.parse_unary()
|
|
218
|
+
left = BinaryOpNode("%", left, right)
|
|
219
|
+
else:
|
|
220
|
+
break
|
|
221
|
+
return left
|
|
222
|
+
|
|
223
|
+
def parse_unary(self) -> AstNode:
|
|
224
|
+
"""Parse unary operators (-)."""
|
|
225
|
+
if self.match(TokenType.MINUS):
|
|
226
|
+
operand = self.parse_unary()
|
|
227
|
+
return UnaryOpNode("-", operand)
|
|
228
|
+
return self.parse_postfix()
|
|
229
|
+
|
|
230
|
+
def parse_postfix(self) -> AstNode:
|
|
231
|
+
"""Parse postfix operators (?, .[...], .field)."""
|
|
232
|
+
expr = self.parse_primary()
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
if self.match(TokenType.QUESTION):
|
|
236
|
+
expr = OptionalNode(expr)
|
|
237
|
+
elif self.check(TokenType.DOT) and self.peek(1).type == TokenType.IDENT:
|
|
238
|
+
self.advance() # consume DOT
|
|
239
|
+
name_tok = self.expect(TokenType.IDENT, "Expected field name")
|
|
240
|
+
expr = FieldNode(name_tok.value, expr)
|
|
241
|
+
elif self.check(TokenType.LBRACKET):
|
|
242
|
+
self.advance()
|
|
243
|
+
if self.match(TokenType.RBRACKET):
|
|
244
|
+
expr = IterateNode(expr)
|
|
245
|
+
elif self.check(TokenType.COLON):
|
|
246
|
+
self.advance()
|
|
247
|
+
end = None if self.check(TokenType.RBRACKET) else self.parse_expr()
|
|
248
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
249
|
+
expr = SliceNode(None, end, expr)
|
|
250
|
+
else:
|
|
251
|
+
index_expr = self.parse_expr()
|
|
252
|
+
if self.match(TokenType.COLON):
|
|
253
|
+
end = None if self.check(TokenType.RBRACKET) else self.parse_expr()
|
|
254
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
255
|
+
expr = SliceNode(index_expr, end, expr)
|
|
256
|
+
else:
|
|
257
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
258
|
+
expr = IndexNode(index_expr, expr)
|
|
259
|
+
else:
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
return expr
|
|
263
|
+
|
|
264
|
+
def parse_primary(self) -> AstNode:
|
|
265
|
+
"""Parse primary expressions."""
|
|
266
|
+
# Recursive descent (..)
|
|
267
|
+
if self.match(TokenType.DOTDOT):
|
|
268
|
+
return RecurseNode()
|
|
269
|
+
|
|
270
|
+
# Identity or field access starting with dot
|
|
271
|
+
if self.match(TokenType.DOT):
|
|
272
|
+
# Check for .[] or .[n] or .[n:m]
|
|
273
|
+
if self.check(TokenType.LBRACKET):
|
|
274
|
+
self.advance()
|
|
275
|
+
if self.match(TokenType.RBRACKET):
|
|
276
|
+
return IterateNode()
|
|
277
|
+
if self.check(TokenType.COLON):
|
|
278
|
+
self.advance()
|
|
279
|
+
end = None if self.check(TokenType.RBRACKET) else self.parse_expr()
|
|
280
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
281
|
+
return SliceNode(None, end)
|
|
282
|
+
index_expr = self.parse_expr()
|
|
283
|
+
if self.match(TokenType.COLON):
|
|
284
|
+
end = None if self.check(TokenType.RBRACKET) else self.parse_expr()
|
|
285
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
286
|
+
return SliceNode(index_expr, end)
|
|
287
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
288
|
+
return IndexNode(index_expr)
|
|
289
|
+
# .field
|
|
290
|
+
if self.check(TokenType.IDENT):
|
|
291
|
+
name = self.advance().value
|
|
292
|
+
return FieldNode(name)
|
|
293
|
+
# Just identity
|
|
294
|
+
return IdentityNode()
|
|
295
|
+
|
|
296
|
+
# Literals
|
|
297
|
+
if self.match(TokenType.TRUE):
|
|
298
|
+
return LiteralNode(True)
|
|
299
|
+
if self.match(TokenType.FALSE):
|
|
300
|
+
return LiteralNode(False)
|
|
301
|
+
if self.match(TokenType.NULL):
|
|
302
|
+
return LiteralNode(None)
|
|
303
|
+
if self.check(TokenType.NUMBER):
|
|
304
|
+
tok = self.advance()
|
|
305
|
+
return LiteralNode(tok.value)
|
|
306
|
+
if self.check(TokenType.STRING):
|
|
307
|
+
tok = self.advance()
|
|
308
|
+
s = tok.value
|
|
309
|
+
# Check for string interpolation
|
|
310
|
+
if isinstance(s, str) and "\\(" in s:
|
|
311
|
+
return self.parse_string_interpolation(s)
|
|
312
|
+
return LiteralNode(s)
|
|
313
|
+
|
|
314
|
+
# Array construction
|
|
315
|
+
if self.match(TokenType.LBRACKET):
|
|
316
|
+
if self.match(TokenType.RBRACKET):
|
|
317
|
+
return ArrayNode()
|
|
318
|
+
elements = self.parse_expr()
|
|
319
|
+
self.expect(TokenType.RBRACKET, "Expected ']'")
|
|
320
|
+
return ArrayNode(elements)
|
|
321
|
+
|
|
322
|
+
# Object construction
|
|
323
|
+
if self.match(TokenType.LBRACE):
|
|
324
|
+
return self.parse_object_construction()
|
|
325
|
+
|
|
326
|
+
# Parentheses
|
|
327
|
+
if self.match(TokenType.LPAREN):
|
|
328
|
+
expr = self.parse_expr()
|
|
329
|
+
self.expect(TokenType.RPAREN, "Expected ')'")
|
|
330
|
+
return ParenNode(expr)
|
|
331
|
+
|
|
332
|
+
# if-then-else
|
|
333
|
+
if self.match(TokenType.IF):
|
|
334
|
+
return self.parse_if()
|
|
335
|
+
|
|
336
|
+
# try-catch
|
|
337
|
+
if self.match(TokenType.TRY):
|
|
338
|
+
body = self.parse_postfix()
|
|
339
|
+
catch_expr = None
|
|
340
|
+
if self.match(TokenType.CATCH):
|
|
341
|
+
catch_expr = self.parse_postfix()
|
|
342
|
+
return TryNode(body, catch_expr)
|
|
343
|
+
|
|
344
|
+
# reduce EXPR as $VAR (INIT; UPDATE)
|
|
345
|
+
if self.match(TokenType.REDUCE):
|
|
346
|
+
expr = self.parse_postfix()
|
|
347
|
+
self.expect(TokenType.AS, "Expected 'as' after reduce expression")
|
|
348
|
+
var_token = self.expect(TokenType.IDENT, "Expected variable name")
|
|
349
|
+
var_name = var_token.value
|
|
350
|
+
if not isinstance(var_name, str) or not var_name.startswith("$"):
|
|
351
|
+
raise ValueError(f"Variable name must start with $ at position {var_token.pos}")
|
|
352
|
+
self.expect(TokenType.LPAREN, "Expected '(' after variable")
|
|
353
|
+
init = self.parse_expr()
|
|
354
|
+
self.expect(TokenType.SEMICOLON, "Expected ';' after init expression")
|
|
355
|
+
update = self.parse_expr()
|
|
356
|
+
self.expect(TokenType.RPAREN, "Expected ')' after update expression")
|
|
357
|
+
return ReduceNode(expr, var_name, init, update)
|
|
358
|
+
|
|
359
|
+
# foreach EXPR as $VAR (INIT; UPDATE) or (INIT; UPDATE; EXTRACT)
|
|
360
|
+
if self.match(TokenType.FOREACH):
|
|
361
|
+
expr = self.parse_postfix()
|
|
362
|
+
self.expect(TokenType.AS, "Expected 'as' after foreach expression")
|
|
363
|
+
var_token = self.expect(TokenType.IDENT, "Expected variable name")
|
|
364
|
+
var_name = var_token.value
|
|
365
|
+
if not isinstance(var_name, str) or not var_name.startswith("$"):
|
|
366
|
+
raise ValueError(f"Variable name must start with $ at position {var_token.pos}")
|
|
367
|
+
self.expect(TokenType.LPAREN, "Expected '(' after variable")
|
|
368
|
+
init = self.parse_expr()
|
|
369
|
+
self.expect(TokenType.SEMICOLON, "Expected ';' after init expression")
|
|
370
|
+
update = self.parse_expr()
|
|
371
|
+
extract = None
|
|
372
|
+
if self.match(TokenType.SEMICOLON):
|
|
373
|
+
extract = self.parse_expr()
|
|
374
|
+
self.expect(TokenType.RPAREN, "Expected ')' after expressions")
|
|
375
|
+
return ForeachNode(expr, var_name, init, update, extract)
|
|
376
|
+
|
|
377
|
+
# not as a standalone filter (when used as a function, not unary operator)
|
|
378
|
+
if self.match(TokenType.NOT):
|
|
379
|
+
return CallNode("not")
|
|
380
|
+
|
|
381
|
+
# Variable reference or function call
|
|
382
|
+
if self.check(TokenType.IDENT):
|
|
383
|
+
tok = self.advance()
|
|
384
|
+
name = tok.value
|
|
385
|
+
|
|
386
|
+
# Variable reference
|
|
387
|
+
if isinstance(name, str) and name.startswith("$"):
|
|
388
|
+
return VarRefNode(name)
|
|
389
|
+
|
|
390
|
+
# Function call with args
|
|
391
|
+
if self.match(TokenType.LPAREN):
|
|
392
|
+
args: list[AstNode] = []
|
|
393
|
+
if not self.check(TokenType.RPAREN):
|
|
394
|
+
args.append(self.parse_expr())
|
|
395
|
+
while self.match(TokenType.SEMICOLON):
|
|
396
|
+
args.append(self.parse_expr())
|
|
397
|
+
self.expect(TokenType.RPAREN, "Expected ')'")
|
|
398
|
+
return CallNode(name, args)
|
|
399
|
+
|
|
400
|
+
# Builtin without parens
|
|
401
|
+
return CallNode(name)
|
|
402
|
+
|
|
403
|
+
raise ValueError(f"Unexpected token {self.peek().type.name} at position {self.peek().pos}")
|
|
404
|
+
|
|
405
|
+
def parse_object_construction(self) -> ObjectNode:
|
|
406
|
+
"""Parse object construction {...}."""
|
|
407
|
+
entries: list[ObjectEntry] = []
|
|
408
|
+
|
|
409
|
+
if not self.check(TokenType.RBRACE):
|
|
410
|
+
while True:
|
|
411
|
+
key: AstNode | str
|
|
412
|
+
value: AstNode
|
|
413
|
+
|
|
414
|
+
# Check for ({(.key): .value}) dynamic key
|
|
415
|
+
if self.match(TokenType.LPAREN):
|
|
416
|
+
key = self.parse_expr()
|
|
417
|
+
self.expect(TokenType.RPAREN, "Expected ')'")
|
|
418
|
+
self.expect(TokenType.COLON, "Expected ':'")
|
|
419
|
+
value = self.parse_object_value()
|
|
420
|
+
elif self.check(TokenType.IDENT):
|
|
421
|
+
ident_tok = self.advance()
|
|
422
|
+
ident = ident_tok.value
|
|
423
|
+
if self.match(TokenType.COLON):
|
|
424
|
+
# {key: value}
|
|
425
|
+
key = ident
|
|
426
|
+
value = self.parse_object_value()
|
|
427
|
+
else:
|
|
428
|
+
# {key} shorthand for {key: .key}
|
|
429
|
+
key = ident
|
|
430
|
+
value = FieldNode(ident)
|
|
431
|
+
elif self.check(TokenType.STRING):
|
|
432
|
+
key_tok = self.advance()
|
|
433
|
+
key = key_tok.value
|
|
434
|
+
self.expect(TokenType.COLON, "Expected ':'")
|
|
435
|
+
value = self.parse_object_value()
|
|
436
|
+
else:
|
|
437
|
+
raise ValueError(f"Expected object key at position {self.peek().pos}")
|
|
438
|
+
|
|
439
|
+
entries.append(ObjectEntry(key, value))
|
|
440
|
+
|
|
441
|
+
if not self.match(TokenType.COMMA):
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
self.expect(TokenType.RBRACE, "Expected '}'")
|
|
445
|
+
return ObjectNode(entries)
|
|
446
|
+
|
|
447
|
+
def parse_object_value(self) -> AstNode:
|
|
448
|
+
"""Parse object value - allows pipes but stops at comma or rbrace."""
|
|
449
|
+
left = self.parse_var_bind()
|
|
450
|
+
while self.match(TokenType.PIPE):
|
|
451
|
+
right = self.parse_var_bind()
|
|
452
|
+
left = PipeNode(left, right)
|
|
453
|
+
return left
|
|
454
|
+
|
|
455
|
+
def parse_if(self) -> CondNode:
|
|
456
|
+
"""Parse if-then-elif-else-end."""
|
|
457
|
+
cond = self.parse_expr()
|
|
458
|
+
self.expect(TokenType.THEN, "Expected 'then'")
|
|
459
|
+
then = self.parse_expr()
|
|
460
|
+
|
|
461
|
+
elifs: list[ElifBranch] = []
|
|
462
|
+
while self.match(TokenType.ELIF):
|
|
463
|
+
elif_cond = self.parse_expr()
|
|
464
|
+
self.expect(TokenType.THEN, "Expected 'then' after elif")
|
|
465
|
+
elif_then = self.parse_expr()
|
|
466
|
+
elifs.append(ElifBranch(elif_cond, elif_then))
|
|
467
|
+
|
|
468
|
+
else_expr = None
|
|
469
|
+
if self.match(TokenType.ELSE):
|
|
470
|
+
else_expr = self.parse_expr()
|
|
471
|
+
|
|
472
|
+
self.expect(TokenType.END, "Expected 'end'")
|
|
473
|
+
return CondNode(cond, then, elifs, else_expr)
|
|
474
|
+
|
|
475
|
+
def parse_string_interpolation(self, s: str) -> StringInterpNode:
|
|
476
|
+
"""Parse a string with interpolation."""
|
|
477
|
+
parts: list[str | AstNode] = []
|
|
478
|
+
current = ""
|
|
479
|
+
i = 0
|
|
480
|
+
|
|
481
|
+
while i < len(s):
|
|
482
|
+
if s[i] == "\\" and i + 1 < len(s) and s[i + 1] == "(":
|
|
483
|
+
if current:
|
|
484
|
+
parts.append(current)
|
|
485
|
+
current = ""
|
|
486
|
+
i += 2
|
|
487
|
+
# Find matching paren
|
|
488
|
+
depth = 1
|
|
489
|
+
expr_str = ""
|
|
490
|
+
while i < len(s) and depth > 0:
|
|
491
|
+
if s[i] == "(":
|
|
492
|
+
depth += 1
|
|
493
|
+
elif s[i] == ")":
|
|
494
|
+
depth -= 1
|
|
495
|
+
if depth > 0:
|
|
496
|
+
expr_str += s[i]
|
|
497
|
+
i += 1
|
|
498
|
+
tokens = tokenize(expr_str)
|
|
499
|
+
parser = Parser(tokens)
|
|
500
|
+
parts.append(parser.parse())
|
|
501
|
+
else:
|
|
502
|
+
current += s[i]
|
|
503
|
+
i += 1
|
|
504
|
+
|
|
505
|
+
if current:
|
|
506
|
+
parts.append(current)
|
|
507
|
+
|
|
508
|
+
return StringInterpNode(parts)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def parse(input_str: str) -> AstNode:
|
|
512
|
+
"""Parse a jq expression string into an AST.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
input_str: The jq expression to parse
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
The root AST node
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
ValueError: If the expression is invalid
|
|
522
|
+
"""
|
|
523
|
+
tokens = tokenize(input_str)
|
|
524
|
+
parser = Parser(tokens)
|
|
525
|
+
return parser.parse()
|