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,578 @@
|
|
|
1
|
+
"""Evaluator for jq expressions.
|
|
2
|
+
|
|
3
|
+
Executes a parsed AST against a value, returning results.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .builtins import call_builtin
|
|
10
|
+
from .types import (
|
|
11
|
+
AstNode,
|
|
12
|
+
EvalContext,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def evaluate(
|
|
17
|
+
value: Any,
|
|
18
|
+
ast: AstNode,
|
|
19
|
+
ctx: EvalContext | None = None,
|
|
20
|
+
) -> list[Any]:
|
|
21
|
+
"""Evaluate an AST against a value, returning a list of results.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
value: The input value to evaluate against
|
|
25
|
+
ast: The AST node to evaluate
|
|
26
|
+
ctx: Optional evaluation context (created if not provided)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A list of result values (jq expressions can produce multiple outputs)
|
|
30
|
+
"""
|
|
31
|
+
if ctx is None:
|
|
32
|
+
ctx = EvalContext()
|
|
33
|
+
|
|
34
|
+
# Initialize root if not set (first evaluation)
|
|
35
|
+
if ctx.root is None:
|
|
36
|
+
ctx = EvalContext(
|
|
37
|
+
vars=ctx.vars,
|
|
38
|
+
limits=ctx.limits,
|
|
39
|
+
env=ctx.env,
|
|
40
|
+
root=value,
|
|
41
|
+
current_path=[],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return _eval_node(value, ast, ctx)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _eval_node(value: Any, ast: AstNode, ctx: EvalContext) -> list[Any]:
|
|
48
|
+
"""Evaluate a single AST node."""
|
|
49
|
+
node_type = ast.type
|
|
50
|
+
|
|
51
|
+
if node_type == "Identity":
|
|
52
|
+
return [value]
|
|
53
|
+
|
|
54
|
+
elif node_type == "Field":
|
|
55
|
+
node = ast # type: FieldNode
|
|
56
|
+
bases = _eval_node(value, node.base, ctx) if node.base else [value]
|
|
57
|
+
results = []
|
|
58
|
+
for v in bases:
|
|
59
|
+
if isinstance(v, dict):
|
|
60
|
+
result = v.get(node.name)
|
|
61
|
+
results.append(result if result is not None else None)
|
|
62
|
+
else:
|
|
63
|
+
results.append(None)
|
|
64
|
+
return results
|
|
65
|
+
|
|
66
|
+
elif node_type == "Index":
|
|
67
|
+
node = ast # type: IndexNode
|
|
68
|
+
bases = _eval_node(value, node.base, ctx) if node.base else [value]
|
|
69
|
+
results = []
|
|
70
|
+
for v in bases:
|
|
71
|
+
indices = _eval_node(v, node.index, ctx)
|
|
72
|
+
for idx in indices:
|
|
73
|
+
if isinstance(idx, int) and isinstance(v, list):
|
|
74
|
+
i = idx if idx >= 0 else len(v) + idx
|
|
75
|
+
if 0 <= i < len(v):
|
|
76
|
+
results.append(v[i])
|
|
77
|
+
else:
|
|
78
|
+
results.append(None)
|
|
79
|
+
elif isinstance(idx, str) and isinstance(v, dict):
|
|
80
|
+
results.append(v.get(idx))
|
|
81
|
+
else:
|
|
82
|
+
results.append(None)
|
|
83
|
+
return results
|
|
84
|
+
|
|
85
|
+
elif node_type == "Slice":
|
|
86
|
+
node = ast # type: SliceNode
|
|
87
|
+
bases = _eval_node(value, node.base, ctx) if node.base else [value]
|
|
88
|
+
results = []
|
|
89
|
+
for v in bases:
|
|
90
|
+
if not isinstance(v, (list, str)):
|
|
91
|
+
results.append(None)
|
|
92
|
+
continue
|
|
93
|
+
length = len(v)
|
|
94
|
+
starts = _eval_node(value, node.start, ctx) if node.start else [0]
|
|
95
|
+
ends = _eval_node(value, node.end, ctx) if node.end else [length]
|
|
96
|
+
for s in starts:
|
|
97
|
+
for e in ends:
|
|
98
|
+
start = _normalize_index(s, length)
|
|
99
|
+
end = _normalize_index(e, length)
|
|
100
|
+
results.append(v[start:end])
|
|
101
|
+
return results
|
|
102
|
+
|
|
103
|
+
elif node_type == "Iterate":
|
|
104
|
+
node = ast # type: IterateNode
|
|
105
|
+
bases = _eval_node(value, node.base, ctx) if node.base else [value]
|
|
106
|
+
results = []
|
|
107
|
+
for v in bases:
|
|
108
|
+
if isinstance(v, list):
|
|
109
|
+
results.extend(v)
|
|
110
|
+
elif isinstance(v, dict):
|
|
111
|
+
results.extend(v.values())
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
elif node_type == "Pipe":
|
|
115
|
+
node = ast # type: PipeNode
|
|
116
|
+
left_results = _eval_node(value, node.left, ctx)
|
|
117
|
+
results = []
|
|
118
|
+
for v in left_results:
|
|
119
|
+
results.extend(_eval_node(v, node.right, ctx))
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
elif node_type == "Comma":
|
|
123
|
+
node = ast # type: CommaNode
|
|
124
|
+
left_results = _eval_node(value, node.left, ctx)
|
|
125
|
+
right_results = _eval_node(value, node.right, ctx)
|
|
126
|
+
return left_results + right_results
|
|
127
|
+
|
|
128
|
+
elif node_type == "Literal":
|
|
129
|
+
node = ast # type: LiteralNode
|
|
130
|
+
return [node.value]
|
|
131
|
+
|
|
132
|
+
elif node_type == "Array":
|
|
133
|
+
node = ast # type: ArrayNode
|
|
134
|
+
if node.elements is None:
|
|
135
|
+
return [[]]
|
|
136
|
+
elements = _eval_node(value, node.elements, ctx)
|
|
137
|
+
return [elements]
|
|
138
|
+
|
|
139
|
+
elif node_type == "Object":
|
|
140
|
+
node = ast # type: ObjectNode
|
|
141
|
+
results: list[dict[str, Any]] = [{}]
|
|
142
|
+
|
|
143
|
+
for entry in node.entries:
|
|
144
|
+
if isinstance(entry.key, str):
|
|
145
|
+
keys = [entry.key]
|
|
146
|
+
else:
|
|
147
|
+
keys = _eval_node(value, entry.key, ctx)
|
|
148
|
+
values = _eval_node(value, entry.value, ctx)
|
|
149
|
+
|
|
150
|
+
new_results = []
|
|
151
|
+
for obj in results:
|
|
152
|
+
for k in keys:
|
|
153
|
+
for v in values:
|
|
154
|
+
new_results.append({**obj, str(k): v})
|
|
155
|
+
results = new_results
|
|
156
|
+
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
elif node_type == "Paren":
|
|
160
|
+
node = ast # type: ParenNode
|
|
161
|
+
return _eval_node(value, node.expr, ctx)
|
|
162
|
+
|
|
163
|
+
elif node_type == "BinaryOp":
|
|
164
|
+
node = ast # type: BinaryOpNode
|
|
165
|
+
return _eval_binary_op(value, node.op, node.left, node.right, ctx)
|
|
166
|
+
|
|
167
|
+
elif node_type == "UnaryOp":
|
|
168
|
+
node = ast # type: UnaryOpNode
|
|
169
|
+
operands = _eval_node(value, node.operand, ctx)
|
|
170
|
+
results = []
|
|
171
|
+
for v in operands:
|
|
172
|
+
if node.op == "-":
|
|
173
|
+
results.append(-v if isinstance(v, (int, float)) else None)
|
|
174
|
+
elif node.op == "not":
|
|
175
|
+
results.append(not _is_truthy(v))
|
|
176
|
+
return results
|
|
177
|
+
|
|
178
|
+
elif node_type == "Cond":
|
|
179
|
+
node = ast # type: CondNode
|
|
180
|
+
conds = _eval_node(value, node.cond, ctx)
|
|
181
|
+
results = []
|
|
182
|
+
for c in conds:
|
|
183
|
+
if _is_truthy(c):
|
|
184
|
+
results.extend(_eval_node(value, node.then, ctx))
|
|
185
|
+
else:
|
|
186
|
+
# Check elifs
|
|
187
|
+
handled = False
|
|
188
|
+
for elif_ in node.elifs:
|
|
189
|
+
elif_conds = _eval_node(value, elif_.cond, ctx)
|
|
190
|
+
if any(_is_truthy(ec) for ec in elif_conds):
|
|
191
|
+
results.extend(_eval_node(value, elif_.then, ctx))
|
|
192
|
+
handled = True
|
|
193
|
+
break
|
|
194
|
+
if not handled:
|
|
195
|
+
if node.else_ is not None:
|
|
196
|
+
results.extend(_eval_node(value, node.else_, ctx))
|
|
197
|
+
else:
|
|
198
|
+
results.append(None)
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
elif node_type == "Try":
|
|
202
|
+
node = ast # type: TryNode
|
|
203
|
+
try:
|
|
204
|
+
return _eval_node(value, node.body, ctx)
|
|
205
|
+
except Exception:
|
|
206
|
+
if node.catch:
|
|
207
|
+
return _eval_node(value, node.catch, ctx)
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
elif node_type == "Call":
|
|
211
|
+
node = ast # type: CallNode
|
|
212
|
+
return call_builtin(value, node.name, node.args, ctx, _eval_node)
|
|
213
|
+
|
|
214
|
+
elif node_type == "VarBind":
|
|
215
|
+
node = ast # type: VarBindNode
|
|
216
|
+
values = _eval_node(value, node.value, ctx)
|
|
217
|
+
results = []
|
|
218
|
+
for v in values:
|
|
219
|
+
new_ctx = EvalContext(
|
|
220
|
+
vars={**ctx.vars, node.name: v},
|
|
221
|
+
limits=ctx.limits,
|
|
222
|
+
env=ctx.env,
|
|
223
|
+
root=ctx.root,
|
|
224
|
+
current_path=ctx.current_path,
|
|
225
|
+
)
|
|
226
|
+
results.extend(_eval_node(value, node.body, new_ctx))
|
|
227
|
+
return results
|
|
228
|
+
|
|
229
|
+
elif node_type == "VarRef":
|
|
230
|
+
node = ast # type: VarRefNode
|
|
231
|
+
# Special case: $ENV returns environment variables
|
|
232
|
+
if node.name == "$ENV":
|
|
233
|
+
return [ctx.env]
|
|
234
|
+
v = ctx.vars.get(node.name)
|
|
235
|
+
return [v] if v is not None else [None]
|
|
236
|
+
|
|
237
|
+
elif node_type == "Recurse":
|
|
238
|
+
# Recursive descent (..)
|
|
239
|
+
results: list[Any] = []
|
|
240
|
+
seen: set[int] = set()
|
|
241
|
+
|
|
242
|
+
def walk(val: Any) -> None:
|
|
243
|
+
if isinstance(val, (dict, list)):
|
|
244
|
+
obj_id = id(val)
|
|
245
|
+
if obj_id in seen:
|
|
246
|
+
return
|
|
247
|
+
seen.add(obj_id)
|
|
248
|
+
results.append(val)
|
|
249
|
+
if isinstance(val, list):
|
|
250
|
+
for item in val:
|
|
251
|
+
walk(item)
|
|
252
|
+
elif isinstance(val, dict):
|
|
253
|
+
for v in val.values():
|
|
254
|
+
walk(v)
|
|
255
|
+
|
|
256
|
+
walk(value)
|
|
257
|
+
return results
|
|
258
|
+
|
|
259
|
+
elif node_type == "Optional":
|
|
260
|
+
node = ast # type: OptionalNode
|
|
261
|
+
try:
|
|
262
|
+
return _eval_node(value, node.expr, ctx)
|
|
263
|
+
except Exception:
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
elif node_type == "StringInterp":
|
|
267
|
+
node = ast # type: StringInterpNode
|
|
268
|
+
parts = []
|
|
269
|
+
for part in node.parts:
|
|
270
|
+
if isinstance(part, str):
|
|
271
|
+
parts.append(part)
|
|
272
|
+
else:
|
|
273
|
+
vals = _eval_node(value, part, ctx)
|
|
274
|
+
parts.append("".join(v if isinstance(v, str) else json.dumps(v) for v in vals))
|
|
275
|
+
return ["".join(parts)]
|
|
276
|
+
|
|
277
|
+
elif node_type == "UpdateOp":
|
|
278
|
+
node = ast # type: UpdateOpNode
|
|
279
|
+
return [_apply_update(value, node.path, node.op, node.value, ctx)]
|
|
280
|
+
|
|
281
|
+
elif node_type == "Reduce":
|
|
282
|
+
node = ast # type: ReduceNode
|
|
283
|
+
items = _eval_node(value, node.expr, ctx)
|
|
284
|
+
accumulator = (
|
|
285
|
+
_eval_node(value, node.init, ctx)[0] if _eval_node(value, node.init, ctx) else None
|
|
286
|
+
)
|
|
287
|
+
for item in items:
|
|
288
|
+
new_ctx = EvalContext(
|
|
289
|
+
vars={**ctx.vars, node.var_name: item},
|
|
290
|
+
limits=ctx.limits,
|
|
291
|
+
env=ctx.env,
|
|
292
|
+
root=ctx.root,
|
|
293
|
+
current_path=ctx.current_path,
|
|
294
|
+
)
|
|
295
|
+
update_results = _eval_node(accumulator, node.update, new_ctx)
|
|
296
|
+
accumulator = update_results[0] if update_results else None
|
|
297
|
+
return [accumulator]
|
|
298
|
+
|
|
299
|
+
elif node_type == "Foreach":
|
|
300
|
+
node = ast # type: ForeachNode
|
|
301
|
+
items = _eval_node(value, node.expr, ctx)
|
|
302
|
+
state = _eval_node(value, node.init, ctx)[0] if _eval_node(value, node.init, ctx) else None
|
|
303
|
+
results = []
|
|
304
|
+
for item in items:
|
|
305
|
+
new_ctx = EvalContext(
|
|
306
|
+
vars={**ctx.vars, node.var_name: item},
|
|
307
|
+
limits=ctx.limits,
|
|
308
|
+
env=ctx.env,
|
|
309
|
+
root=ctx.root,
|
|
310
|
+
current_path=ctx.current_path,
|
|
311
|
+
)
|
|
312
|
+
state_results = _eval_node(state, node.update, new_ctx)
|
|
313
|
+
state = state_results[0] if state_results else None
|
|
314
|
+
if node.extract:
|
|
315
|
+
extracted = _eval_node(state, node.extract, new_ctx)
|
|
316
|
+
results.extend(extracted)
|
|
317
|
+
else:
|
|
318
|
+
results.append(state)
|
|
319
|
+
return results
|
|
320
|
+
|
|
321
|
+
raise ValueError(f"Unknown AST node type: {node_type}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _normalize_index(idx: int, length: int) -> int:
|
|
325
|
+
"""Normalize a slice index."""
|
|
326
|
+
if idx < 0:
|
|
327
|
+
return max(0, length + idx)
|
|
328
|
+
return min(idx, length)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _is_truthy(v: Any) -> bool:
|
|
332
|
+
"""Check if a value is truthy in jq terms."""
|
|
333
|
+
return v is not None and v is not False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _eval_binary_op(
|
|
337
|
+
value: Any,
|
|
338
|
+
op: str,
|
|
339
|
+
left: AstNode,
|
|
340
|
+
right: AstNode,
|
|
341
|
+
ctx: EvalContext,
|
|
342
|
+
) -> list[Any]:
|
|
343
|
+
"""Evaluate a binary operation."""
|
|
344
|
+
# Short-circuit for 'and' and 'or'
|
|
345
|
+
if op == "and":
|
|
346
|
+
left_vals = _eval_node(value, left, ctx)
|
|
347
|
+
results = []
|
|
348
|
+
for lv in left_vals:
|
|
349
|
+
if not _is_truthy(lv):
|
|
350
|
+
results.append(False)
|
|
351
|
+
else:
|
|
352
|
+
right_vals = _eval_node(value, right, ctx)
|
|
353
|
+
results.extend(_is_truthy(rv) for rv in right_vals)
|
|
354
|
+
return results
|
|
355
|
+
|
|
356
|
+
if op == "or":
|
|
357
|
+
left_vals = _eval_node(value, left, ctx)
|
|
358
|
+
results = []
|
|
359
|
+
for lv in left_vals:
|
|
360
|
+
if _is_truthy(lv):
|
|
361
|
+
results.append(True)
|
|
362
|
+
else:
|
|
363
|
+
right_vals = _eval_node(value, right, ctx)
|
|
364
|
+
results.extend(_is_truthy(rv) for rv in right_vals)
|
|
365
|
+
return results
|
|
366
|
+
|
|
367
|
+
if op == "//":
|
|
368
|
+
left_vals = _eval_node(value, left, ctx)
|
|
369
|
+
non_null = [v for v in left_vals if v is not None and v is not False]
|
|
370
|
+
if non_null:
|
|
371
|
+
return non_null
|
|
372
|
+
return _eval_node(value, right, ctx)
|
|
373
|
+
|
|
374
|
+
left_vals = _eval_node(value, left, ctx)
|
|
375
|
+
right_vals = _eval_node(value, right, ctx)
|
|
376
|
+
|
|
377
|
+
results = []
|
|
378
|
+
for lv in left_vals:
|
|
379
|
+
for rv in right_vals:
|
|
380
|
+
if op == "+":
|
|
381
|
+
if isinstance(lv, (int, float)) and isinstance(rv, (int, float)):
|
|
382
|
+
results.append(lv + rv)
|
|
383
|
+
elif isinstance(lv, str) and isinstance(rv, str):
|
|
384
|
+
results.append(lv + rv)
|
|
385
|
+
elif isinstance(lv, list) and isinstance(rv, list):
|
|
386
|
+
results.append(lv + rv)
|
|
387
|
+
elif isinstance(lv, dict) and isinstance(rv, dict):
|
|
388
|
+
results.append({**lv, **rv})
|
|
389
|
+
else:
|
|
390
|
+
results.append(None)
|
|
391
|
+
elif op == "-":
|
|
392
|
+
if isinstance(lv, (int, float)) and isinstance(rv, (int, float)):
|
|
393
|
+
results.append(lv - rv)
|
|
394
|
+
elif isinstance(lv, list) and isinstance(rv, list):
|
|
395
|
+
# Subtract elements in rv from lv
|
|
396
|
+
r_set = {json.dumps(x, sort_keys=True) for x in rv}
|
|
397
|
+
results.append([x for x in lv if json.dumps(x, sort_keys=True) not in r_set])
|
|
398
|
+
else:
|
|
399
|
+
results.append(None)
|
|
400
|
+
elif op == "*":
|
|
401
|
+
if isinstance(lv, (int, float)) and isinstance(rv, (int, float)):
|
|
402
|
+
results.append(lv * rv)
|
|
403
|
+
elif isinstance(lv, str) and isinstance(rv, int):
|
|
404
|
+
results.append(lv * rv)
|
|
405
|
+
elif isinstance(lv, dict) and isinstance(rv, dict):
|
|
406
|
+
results.append(_deep_merge(lv, rv))
|
|
407
|
+
else:
|
|
408
|
+
results.append(None)
|
|
409
|
+
elif op == "/":
|
|
410
|
+
if isinstance(lv, (int, float)) and isinstance(rv, (int, float)):
|
|
411
|
+
results.append(lv / rv if rv != 0 else None)
|
|
412
|
+
elif isinstance(lv, str) and isinstance(rv, str):
|
|
413
|
+
results.append(lv.split(rv))
|
|
414
|
+
else:
|
|
415
|
+
results.append(None)
|
|
416
|
+
elif op == "%":
|
|
417
|
+
if isinstance(lv, (int, float)) and isinstance(rv, (int, float)):
|
|
418
|
+
results.append(lv % rv if rv != 0 else None)
|
|
419
|
+
else:
|
|
420
|
+
results.append(None)
|
|
421
|
+
elif op == "==":
|
|
422
|
+
results.append(_deep_equal(lv, rv))
|
|
423
|
+
elif op == "!=":
|
|
424
|
+
results.append(not _deep_equal(lv, rv))
|
|
425
|
+
elif op == "<":
|
|
426
|
+
results.append(_compare(lv, rv) < 0)
|
|
427
|
+
elif op == "<=":
|
|
428
|
+
results.append(_compare(lv, rv) <= 0)
|
|
429
|
+
elif op == ">":
|
|
430
|
+
results.append(_compare(lv, rv) > 0)
|
|
431
|
+
elif op == ">=":
|
|
432
|
+
results.append(_compare(lv, rv) >= 0)
|
|
433
|
+
else:
|
|
434
|
+
results.append(None)
|
|
435
|
+
|
|
436
|
+
return results
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _deep_equal(a: Any, b: Any) -> bool:
|
|
440
|
+
"""Deep equality check."""
|
|
441
|
+
return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _compare(a: Any, b: Any) -> int:
|
|
445
|
+
"""Compare two values jq-style."""
|
|
446
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
447
|
+
return -1 if a < b else (1 if a > b else 0)
|
|
448
|
+
if isinstance(a, str) and isinstance(b, str):
|
|
449
|
+
return -1 if a < b else (1 if a > b else 0)
|
|
450
|
+
return 0
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _deep_merge(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
454
|
+
"""Deep merge two dictionaries."""
|
|
455
|
+
result = dict(a)
|
|
456
|
+
for key, val in b.items():
|
|
457
|
+
if key in result and isinstance(result[key], dict) and isinstance(val, dict):
|
|
458
|
+
result[key] = _deep_merge(result[key], val)
|
|
459
|
+
else:
|
|
460
|
+
result[key] = val
|
|
461
|
+
return result
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _apply_update(
|
|
465
|
+
root: Any,
|
|
466
|
+
path_expr: AstNode,
|
|
467
|
+
op: str,
|
|
468
|
+
value_expr: AstNode,
|
|
469
|
+
ctx: EvalContext,
|
|
470
|
+
) -> Any:
|
|
471
|
+
"""Apply an update operation."""
|
|
472
|
+
|
|
473
|
+
def compute_new_value(current: Any, new_val: Any) -> Any:
|
|
474
|
+
if op == "=":
|
|
475
|
+
return new_val
|
|
476
|
+
elif op == "|=":
|
|
477
|
+
# For |=, evaluate value_expr with current as input
|
|
478
|
+
results = _eval_node(current, value_expr, ctx)
|
|
479
|
+
return results[0] if results else None
|
|
480
|
+
elif op == "+=":
|
|
481
|
+
if isinstance(current, (int, float)) and isinstance(new_val, (int, float)):
|
|
482
|
+
return current + new_val
|
|
483
|
+
if isinstance(current, str) and isinstance(new_val, str):
|
|
484
|
+
return current + new_val
|
|
485
|
+
if isinstance(current, list) and isinstance(new_val, list):
|
|
486
|
+
return current + new_val
|
|
487
|
+
if isinstance(current, dict) and isinstance(new_val, dict):
|
|
488
|
+
return {**current, **new_val}
|
|
489
|
+
return new_val
|
|
490
|
+
elif op == "-=":
|
|
491
|
+
if isinstance(current, (int, float)) and isinstance(new_val, (int, float)):
|
|
492
|
+
return current - new_val
|
|
493
|
+
return current
|
|
494
|
+
elif op == "*=":
|
|
495
|
+
if isinstance(current, (int, float)) and isinstance(new_val, (int, float)):
|
|
496
|
+
return current * new_val
|
|
497
|
+
return current
|
|
498
|
+
elif op == "/=":
|
|
499
|
+
if isinstance(current, (int, float)) and isinstance(new_val, (int, float)):
|
|
500
|
+
return current / new_val if new_val != 0 else current
|
|
501
|
+
return current
|
|
502
|
+
elif op == "%=":
|
|
503
|
+
if isinstance(current, (int, float)) and isinstance(new_val, (int, float)):
|
|
504
|
+
return current % new_val if new_val != 0 else current
|
|
505
|
+
return current
|
|
506
|
+
elif op == "//=":
|
|
507
|
+
return current if current is not None and current is not False else new_val
|
|
508
|
+
return new_val
|
|
509
|
+
|
|
510
|
+
def update_recursive(val: Any, path: AstNode, transform) -> Any:
|
|
511
|
+
if path.type == "Identity":
|
|
512
|
+
return transform(val)
|
|
513
|
+
elif path.type == "Field":
|
|
514
|
+
field_node = path # type: FieldNode
|
|
515
|
+
if field_node.base:
|
|
516
|
+
return update_recursive(
|
|
517
|
+
val,
|
|
518
|
+
field_node.base,
|
|
519
|
+
lambda base_val: (
|
|
520
|
+
{**base_val, field_node.name: transform(base_val.get(field_node.name))}
|
|
521
|
+
if isinstance(base_val, dict)
|
|
522
|
+
else base_val
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
if isinstance(val, dict):
|
|
526
|
+
return {**val, field_node.name: transform(val.get(field_node.name))}
|
|
527
|
+
return val
|
|
528
|
+
elif path.type == "Index":
|
|
529
|
+
index_node = path # type: IndexNode
|
|
530
|
+
indices = _eval_node(root, index_node.index, ctx)
|
|
531
|
+
idx = indices[0] if indices else None
|
|
532
|
+
|
|
533
|
+
if index_node.base:
|
|
534
|
+
return update_recursive(
|
|
535
|
+
val,
|
|
536
|
+
index_node.base,
|
|
537
|
+
lambda base_val: (_update_at_index(base_val, idx, transform)),
|
|
538
|
+
)
|
|
539
|
+
return _update_at_index(val, idx, transform)
|
|
540
|
+
elif path.type == "Iterate":
|
|
541
|
+
iter_node = path # type: IterateNode
|
|
542
|
+
apply_to_container = lambda container: (
|
|
543
|
+
[transform(item) for item in container]
|
|
544
|
+
if isinstance(container, list)
|
|
545
|
+
else {k: transform(v) for k, v in container.items()}
|
|
546
|
+
if isinstance(container, dict)
|
|
547
|
+
else container
|
|
548
|
+
)
|
|
549
|
+
if iter_node.base:
|
|
550
|
+
return update_recursive(val, iter_node.base, apply_to_container)
|
|
551
|
+
return apply_to_container(val)
|
|
552
|
+
elif path.type == "Pipe":
|
|
553
|
+
pipe_node = path # type: PipeNode
|
|
554
|
+
left_result = update_recursive(val, pipe_node.left, lambda x: x)
|
|
555
|
+
return update_recursive(left_result, pipe_node.right, transform)
|
|
556
|
+
else:
|
|
557
|
+
return transform(val)
|
|
558
|
+
|
|
559
|
+
def transformer(current: Any) -> Any:
|
|
560
|
+
if op == "|=":
|
|
561
|
+
return compute_new_value(current, current)
|
|
562
|
+
new_vals = _eval_node(root, value_expr, ctx)
|
|
563
|
+
return compute_new_value(current, new_vals[0] if new_vals else None)
|
|
564
|
+
|
|
565
|
+
return update_recursive(root, path_expr, transformer)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _update_at_index(val: Any, idx: Any, transform) -> Any:
|
|
569
|
+
"""Update a value at an index."""
|
|
570
|
+
if isinstance(idx, int) and isinstance(val, list):
|
|
571
|
+
arr = list(val)
|
|
572
|
+
i = idx if idx >= 0 else len(arr) + idx
|
|
573
|
+
if 0 <= i < len(arr):
|
|
574
|
+
arr[i] = transform(arr[i])
|
|
575
|
+
return arr
|
|
576
|
+
if isinstance(idx, str) and isinstance(val, dict):
|
|
577
|
+
return {**val, idx: transform(val.get(idx))}
|
|
578
|
+
return val
|