just-bash 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. just_bash-0.1.5.dist-info/WHEEL +4 -0
@@ -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