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,1283 @@
1
+ """Builtin functions for the query engine.
2
+
3
+ This module provides all the builtin functions available in jq expressions.
4
+ """
5
+
6
+ import base64
7
+ import json
8
+ import math
9
+ import re
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+ from urllib.parse import quote as uri_quote
13
+
14
+ from ..types import AstNode, EvalContext
15
+
16
+ # Type for the evaluate function passed from evaluator
17
+ EvalFunc = Callable[[Any, AstNode, EvalContext], list[Any]]
18
+
19
+
20
+ def call_builtin(
21
+ value: Any,
22
+ name: str,
23
+ args: list[AstNode],
24
+ ctx: EvalContext,
25
+ eval_fn: EvalFunc,
26
+ ) -> list[Any]:
27
+ """Call a builtin function.
28
+
29
+ Args:
30
+ value: The current input value
31
+ name: The function name
32
+ args: The function arguments (as AST nodes)
33
+ ctx: The evaluation context
34
+ eval_fn: Function to evaluate AST nodes
35
+
36
+ Returns:
37
+ A list of result values
38
+
39
+ Raises:
40
+ ValueError: If the function is unknown
41
+ """
42
+ # Core functions
43
+ if name == "keys":
44
+ if isinstance(value, list):
45
+ return [list(range(len(value)))]
46
+ if isinstance(value, dict):
47
+ return [sorted(value.keys())]
48
+ return [None]
49
+
50
+ if name == "keys_unsorted":
51
+ if isinstance(value, list):
52
+ return [list(range(len(value)))]
53
+ if isinstance(value, dict):
54
+ return [list(value.keys())]
55
+ return [None]
56
+
57
+ if name == "values":
58
+ if isinstance(value, list):
59
+ return [value]
60
+ if isinstance(value, dict):
61
+ return [list(value.values())]
62
+ return [None]
63
+
64
+ if name == "length":
65
+ if isinstance(value, str):
66
+ return [len(value)]
67
+ if isinstance(value, (list, dict)):
68
+ return [len(value)]
69
+ if value is None:
70
+ return [0]
71
+ return [None]
72
+
73
+ if name == "utf8bytelength":
74
+ if isinstance(value, str):
75
+ return [len(value.encode("utf-8"))]
76
+ return [None]
77
+
78
+ if name == "type":
79
+ if value is None:
80
+ return ["null"]
81
+ if isinstance(value, bool):
82
+ return ["boolean"]
83
+ if isinstance(value, (int, float)):
84
+ return ["number"]
85
+ if isinstance(value, str):
86
+ return ["string"]
87
+ if isinstance(value, list):
88
+ return ["array"]
89
+ if isinstance(value, dict):
90
+ return ["object"]
91
+ return ["null"]
92
+
93
+ if name == "empty":
94
+ return []
95
+
96
+ if name == "error":
97
+ msg = eval_fn(value, args[0], ctx)[0] if args else value
98
+ raise ValueError(str(msg))
99
+
100
+ if name == "not":
101
+ return [not _is_truthy(value)]
102
+
103
+ if name == "null":
104
+ return [None]
105
+
106
+ if name == "true":
107
+ return [True]
108
+
109
+ if name == "false":
110
+ return [False]
111
+
112
+ if name == "first":
113
+ if args:
114
+ results = eval_fn(value, args[0], ctx)
115
+ return [results[0]] if results else []
116
+ if isinstance(value, list) and value:
117
+ return [value[0]]
118
+ return [None]
119
+
120
+ if name == "last":
121
+ if args:
122
+ results = eval_fn(value, args[0], ctx)
123
+ return [results[-1]] if results else []
124
+ if isinstance(value, list) and value:
125
+ return [value[-1]]
126
+ return [None]
127
+
128
+ if name == "nth":
129
+ if not args:
130
+ return [None]
131
+ ns = eval_fn(value, args[0], ctx)
132
+ n = ns[0] if ns else 0
133
+ if len(args) > 1:
134
+ results = eval_fn(value, args[1], ctx)
135
+ return [results[n]] if isinstance(n, int) and 0 <= n < len(results) else []
136
+ if isinstance(value, list):
137
+ return [value[n]] if isinstance(n, int) and 0 <= n < len(value) else [None]
138
+ return [None]
139
+
140
+ if name == "range":
141
+ if not args:
142
+ return []
143
+ starts = eval_fn(value, args[0], ctx)
144
+ if len(args) == 1:
145
+ n = starts[0] if starts else 0
146
+ return list(range(int(n)))
147
+ ends = eval_fn(value, args[1], ctx)
148
+ start = int(starts[0]) if starts else 0
149
+ end = int(ends[0]) if ends else 0
150
+ return list(range(start, end))
151
+
152
+ if name == "reverse":
153
+ if isinstance(value, list):
154
+ return [list(reversed(value))]
155
+ if isinstance(value, str):
156
+ return [value[::-1]]
157
+ return [None]
158
+
159
+ if name == "sort":
160
+ if isinstance(value, list):
161
+ return [sorted(value, key=_jq_sort_key)]
162
+ return [None]
163
+
164
+ if name == "sort_by":
165
+ if not isinstance(value, list) or not args:
166
+ return [None]
167
+ items = [(eval_fn(item, args[0], ctx), item) for item in value]
168
+ sorted_items = sorted(items, key=lambda x: _jq_sort_key(x[0][0] if x[0] else None))
169
+ return [[item for _, item in sorted_items]]
170
+
171
+ if name == "unique":
172
+ if isinstance(value, list):
173
+ seen = set()
174
+ result = []
175
+ for item in value:
176
+ key = json.dumps(item, sort_keys=True)
177
+ if key not in seen:
178
+ seen.add(key)
179
+ result.append(item)
180
+ return [result]
181
+ return [None]
182
+
183
+ if name == "unique_by":
184
+ if not isinstance(value, list) or not args:
185
+ return [None]
186
+ seen = set()
187
+ result = []
188
+ for item in value:
189
+ key_vals = eval_fn(item, args[0], ctx)
190
+ key = json.dumps(key_vals[0] if key_vals else None, sort_keys=True)
191
+ if key not in seen:
192
+ seen.add(key)
193
+ result.append(item)
194
+ return [result]
195
+
196
+ if name == "group_by":
197
+ if not isinstance(value, list) or not args:
198
+ return [None]
199
+ groups: dict[str, list[Any]] = {}
200
+ for item in value:
201
+ key_vals = eval_fn(item, args[0], ctx)
202
+ key = json.dumps(key_vals[0] if key_vals else None, sort_keys=True)
203
+ if key not in groups:
204
+ groups[key] = []
205
+ groups[key].append(item)
206
+ return [list(groups.values())]
207
+
208
+ if name == "max":
209
+ if isinstance(value, list) and value:
210
+ return [max(value, key=_jq_sort_key)]
211
+ return [None]
212
+
213
+ if name == "max_by":
214
+ if not isinstance(value, list) or not value or not args:
215
+ return [None]
216
+ items = [(eval_fn(item, args[0], ctx), item) for item in value]
217
+ max_item = max(items, key=lambda x: _jq_sort_key(x[0][0] if x[0] else None))
218
+ return [max_item[1]]
219
+
220
+ if name == "min":
221
+ if isinstance(value, list) and value:
222
+ return [min(value, key=_jq_sort_key)]
223
+ return [None]
224
+
225
+ if name == "min_by":
226
+ if not isinstance(value, list) or not value or not args:
227
+ return [None]
228
+ items = [(eval_fn(item, args[0], ctx), item) for item in value]
229
+ min_item = min(items, key=lambda x: _jq_sort_key(x[0][0] if x[0] else None))
230
+ return [min_item[1]]
231
+
232
+ if name == "flatten":
233
+ if not isinstance(value, list):
234
+ return [None]
235
+ depth = float("inf")
236
+ if args:
237
+ depth_vals = eval_fn(value, args[0], ctx)
238
+ depth = depth_vals[0] if depth_vals else float("inf")
239
+ result = _flatten(value, int(depth) if depth != float("inf") else None)
240
+ return [result]
241
+
242
+ if name == "add":
243
+ if isinstance(value, list):
244
+ if not value:
245
+ return [None]
246
+ if all(isinstance(x, (int, float)) for x in value):
247
+ return [sum(value)]
248
+ if all(isinstance(x, str) for x in value):
249
+ return ["".join(value)]
250
+ if all(isinstance(x, list) for x in value):
251
+ result = []
252
+ for x in value:
253
+ result.extend(x)
254
+ return [result]
255
+ if all(isinstance(x, dict) for x in value):
256
+ result = {}
257
+ for x in value:
258
+ result.update(x)
259
+ return [result]
260
+ return [None]
261
+
262
+ if name == "any":
263
+ if args:
264
+ if isinstance(value, list):
265
+ return [
266
+ any(
267
+ _is_truthy(eval_fn(item, args[0], ctx)[0])
268
+ for item in value
269
+ if eval_fn(item, args[0], ctx)
270
+ )
271
+ ]
272
+ return [False]
273
+ if isinstance(value, list):
274
+ return [any(_is_truthy(x) for x in value)]
275
+ return [False]
276
+
277
+ if name == "all":
278
+ if args:
279
+ if isinstance(value, list):
280
+ return [
281
+ all(
282
+ _is_truthy(eval_fn(item, args[0], ctx)[0])
283
+ for item in value
284
+ if eval_fn(item, args[0], ctx)
285
+ )
286
+ ]
287
+ return [True]
288
+ if isinstance(value, list):
289
+ return [all(_is_truthy(x) for x in value)]
290
+ return [True]
291
+
292
+ if name == "select":
293
+ if not args:
294
+ return [value]
295
+ conds = eval_fn(value, args[0], ctx)
296
+ return [value] if any(_is_truthy(c) for c in conds) else []
297
+
298
+ if name == "map":
299
+ if not args or not isinstance(value, list):
300
+ return [None]
301
+ results = []
302
+ for item in value:
303
+ results.extend(eval_fn(item, args[0], ctx))
304
+ return [results]
305
+
306
+ if name == "map_values":
307
+ if not args:
308
+ return [None]
309
+ if isinstance(value, list):
310
+ results = []
311
+ for item in value:
312
+ item_results = eval_fn(item, args[0], ctx)
313
+ results.extend(item_results)
314
+ return [results]
315
+ if isinstance(value, dict):
316
+ result = {}
317
+ for k, v in value.items():
318
+ mapped = eval_fn(v, args[0], ctx)
319
+ if mapped:
320
+ result[k] = mapped[0]
321
+ return [result]
322
+ return [None]
323
+
324
+ if name == "has":
325
+ if not args:
326
+ return [False]
327
+ keys = eval_fn(value, args[0], ctx)
328
+ key = keys[0] if keys else None
329
+ if isinstance(value, list) and isinstance(key, int):
330
+ return [0 <= key < len(value)]
331
+ if isinstance(value, dict) and isinstance(key, str):
332
+ return [key in value]
333
+ return [False]
334
+
335
+ if name == "in":
336
+ if not args:
337
+ return [False]
338
+ objs = eval_fn(value, args[0], ctx)
339
+ obj = objs[0] if objs else None
340
+ if isinstance(obj, list) and isinstance(value, int):
341
+ return [0 <= value < len(obj)]
342
+ if isinstance(obj, dict) and isinstance(value, str):
343
+ return [value in obj]
344
+ return [False]
345
+
346
+ if name == "contains":
347
+ if not args:
348
+ return [False]
349
+ others = eval_fn(value, args[0], ctx)
350
+ other = others[0] if others else None
351
+ return [_contains_deep(value, other)]
352
+
353
+ if name == "inside":
354
+ if not args:
355
+ return [False]
356
+ others = eval_fn(value, args[0], ctx)
357
+ other = others[0] if others else None
358
+ return [_contains_deep(other, value)]
359
+
360
+ if name == "getpath":
361
+ if not args:
362
+ return [None]
363
+ paths = eval_fn(value, args[0], ctx)
364
+ path = paths[0] if paths else []
365
+ current = value
366
+ for key in path:
367
+ if current is None:
368
+ return [None]
369
+ if isinstance(current, list) and isinstance(key, int):
370
+ current = current[key] if 0 <= key < len(current) else None
371
+ elif isinstance(current, dict) and isinstance(key, str):
372
+ current = current.get(key)
373
+ else:
374
+ return [None]
375
+ return [current]
376
+
377
+ if name == "setpath":
378
+ if len(args) < 2:
379
+ return [None]
380
+ paths = eval_fn(value, args[0], ctx)
381
+ path = paths[0] if paths else []
382
+ vals = eval_fn(value, args[1], ctx)
383
+ new_val = vals[0] if vals else None
384
+ return [_set_path(value, path, new_val)]
385
+
386
+ if name == "delpaths":
387
+ if not args:
388
+ return [value]
389
+ path_lists = eval_fn(value, args[0], ctx)
390
+ paths = path_lists[0] if path_lists else []
391
+ result = value
392
+ # Delete longest paths first to avoid index shifting issues
393
+ for path in sorted(paths, key=len, reverse=True):
394
+ result = _delete_path(result, path)
395
+ return [result]
396
+
397
+ if name == "path":
398
+ if not args:
399
+ return [[]]
400
+ # Collect all paths that match the expression
401
+ paths = []
402
+ _collect_paths(value, args[0], ctx, eval_fn, [], paths)
403
+ return paths
404
+
405
+ if name == "del":
406
+ if not args:
407
+ return [value]
408
+ return [_apply_del(value, args[0], ctx, eval_fn)]
409
+
410
+ if name == "paths":
411
+ paths = _get_all_paths(value, [])
412
+ if args:
413
+ # Filter paths by predicate
414
+ filtered = []
415
+ for p in paths:
416
+ v = _get_value_at_path(value, p)
417
+ results = eval_fn(v, args[0], ctx)
418
+ if any(_is_truthy(r) for r in results):
419
+ filtered.append(p)
420
+ return filtered
421
+ return paths
422
+
423
+ if name == "leaf_paths":
424
+ return [_get_leaf_paths(value, [])]
425
+
426
+ if name == "to_entries":
427
+ if isinstance(value, dict):
428
+ return [[{"key": k, "value": v} for k, v in value.items()]]
429
+ return [None]
430
+
431
+ if name == "from_entries":
432
+ if isinstance(value, list):
433
+ result = {}
434
+ for item in value:
435
+ if isinstance(item, dict):
436
+ key = item.get("key") or item.get("name") or item.get("k")
437
+ val = item.get("value") if "value" in item else item.get("v")
438
+ if key is not None:
439
+ result[str(key)] = val
440
+ return [result]
441
+ return [None]
442
+
443
+ if name == "with_entries":
444
+ if not args:
445
+ return [value]
446
+ if isinstance(value, dict):
447
+ entries = [{"key": k, "value": v} for k, v in value.items()]
448
+ new_entries = []
449
+ for entry in entries:
450
+ results = eval_fn(entry, args[0], ctx)
451
+ new_entries.extend(results)
452
+ result = {}
453
+ for item in new_entries:
454
+ if isinstance(item, dict):
455
+ key = item.get("key") or item.get("name") or item.get("k")
456
+ val = item.get("value") if "value" in item else item.get("v")
457
+ if key is not None:
458
+ result[str(key)] = val
459
+ return [result]
460
+ return [None]
461
+
462
+ # String functions
463
+ if name == "join":
464
+ if not isinstance(value, list):
465
+ return [None]
466
+ seps = eval_fn(value, args[0], ctx) if args else [""]
467
+ sep = str(seps[0]) if seps else ""
468
+ return [sep.join(v if isinstance(v, str) else json.dumps(v) for v in value)]
469
+
470
+ if name == "split":
471
+ if not isinstance(value, str) or not args:
472
+ return [None]
473
+ seps = eval_fn(value, args[0], ctx)
474
+ sep = str(seps[0]) if seps else ""
475
+ return [value.split(sep)]
476
+
477
+ if name == "test":
478
+ if not isinstance(value, str) or not args:
479
+ return [False]
480
+ patterns = eval_fn(value, args[0], ctx)
481
+ pattern = str(patterns[0]) if patterns else ""
482
+ try:
483
+ flags = eval_fn(value, args[1], ctx)[0] if len(args) > 1 else ""
484
+ re_flags = _get_re_flags(flags)
485
+ return [bool(re.search(pattern, value, re_flags))]
486
+ except re.error:
487
+ return [False]
488
+
489
+ if name == "match":
490
+ if not isinstance(value, str) or not args:
491
+ return [None]
492
+ patterns = eval_fn(value, args[0], ctx)
493
+ pattern = str(patterns[0]) if patterns else ""
494
+ try:
495
+ flags = eval_fn(value, args[1], ctx)[0] if len(args) > 1 else ""
496
+ re_flags = _get_re_flags(flags)
497
+ m = re.search(pattern, value, re_flags)
498
+ if not m:
499
+ return []
500
+ return [
501
+ {
502
+ "offset": m.start(),
503
+ "length": len(m.group()),
504
+ "string": m.group(),
505
+ "captures": [
506
+ {
507
+ "offset": m.start(i + 1) if m.group(i + 1) else None,
508
+ "length": len(m.group(i + 1)) if m.group(i + 1) else 0,
509
+ "string": m.group(i + 1) or "",
510
+ "name": None,
511
+ }
512
+ for i in range(m.lastindex or 0)
513
+ ],
514
+ }
515
+ ]
516
+ except re.error:
517
+ return [None]
518
+
519
+ if name == "capture":
520
+ if not isinstance(value, str) or not args:
521
+ return [None]
522
+ patterns = eval_fn(value, args[0], ctx)
523
+ pattern = str(patterns[0]) if patterns else ""
524
+ try:
525
+ flags = eval_fn(value, args[1], ctx)[0] if len(args) > 1 else ""
526
+ re_flags = _get_re_flags(flags)
527
+ m = re.search(pattern, value, re_flags)
528
+ if not m or not m.groupdict():
529
+ return [{}]
530
+ return [m.groupdict()]
531
+ except re.error:
532
+ return [None]
533
+
534
+ if name == "sub":
535
+ if not isinstance(value, str) or len(args) < 2:
536
+ return [None]
537
+ patterns = eval_fn(value, args[0], ctx)
538
+ replacements = eval_fn(value, args[1], ctx)
539
+ pattern = str(patterns[0]) if patterns else ""
540
+ replacement = str(replacements[0]) if replacements else ""
541
+ try:
542
+ flags = eval_fn(value, args[2], ctx)[0] if len(args) > 2 else ""
543
+ re_flags = _get_re_flags(flags)
544
+ return [re.sub(pattern, replacement, value, count=1, flags=re_flags)]
545
+ except re.error:
546
+ return [value]
547
+
548
+ if name == "gsub":
549
+ if not isinstance(value, str) or len(args) < 2:
550
+ return [None]
551
+ patterns = eval_fn(value, args[0], ctx)
552
+ replacements = eval_fn(value, args[1], ctx)
553
+ pattern = str(patterns[0]) if patterns else ""
554
+ replacement = str(replacements[0]) if replacements else ""
555
+ try:
556
+ flags = eval_fn(value, args[2], ctx)[0] if len(args) > 2 else "g"
557
+ re_flags = _get_re_flags(flags)
558
+ return [re.sub(pattern, replacement, value, flags=re_flags)]
559
+ except re.error:
560
+ return [value]
561
+
562
+ if name == "ascii_downcase":
563
+ if isinstance(value, str):
564
+ return [value.lower()]
565
+ return [None]
566
+
567
+ if name == "ascii_upcase":
568
+ if isinstance(value, str):
569
+ return [value.upper()]
570
+ return [None]
571
+
572
+ if name == "ltrimstr":
573
+ if not isinstance(value, str) or not args:
574
+ return [value]
575
+ prefixes = eval_fn(value, args[0], ctx)
576
+ prefix = str(prefixes[0]) if prefixes else ""
577
+ return [value[len(prefix) :] if value.startswith(prefix) else value]
578
+
579
+ if name == "rtrimstr":
580
+ if not isinstance(value, str) or not args:
581
+ return [value]
582
+ suffixes = eval_fn(value, args[0], ctx)
583
+ suffix = str(suffixes[0]) if suffixes else ""
584
+ return [value[: -len(suffix)] if value.endswith(suffix) and suffix else value]
585
+
586
+ if name == "trim":
587
+ if isinstance(value, str):
588
+ return [value.strip()]
589
+ return [value]
590
+
591
+ if name == "startswith":
592
+ if not isinstance(value, str) or not args:
593
+ return [False]
594
+ prefixes = eval_fn(value, args[0], ctx)
595
+ prefix = str(prefixes[0]) if prefixes else ""
596
+ return [value.startswith(prefix)]
597
+
598
+ if name == "endswith":
599
+ if not isinstance(value, str) or not args:
600
+ return [False]
601
+ suffixes = eval_fn(value, args[0], ctx)
602
+ suffix = str(suffixes[0]) if suffixes else ""
603
+ return [value.endswith(suffix)]
604
+
605
+ if name == "index":
606
+ if not args:
607
+ return [None]
608
+ needles = eval_fn(value, args[0], ctx)
609
+ needle = needles[0] if needles else None
610
+ if isinstance(value, str) and isinstance(needle, str):
611
+ idx = value.find(needle)
612
+ return [idx if idx >= 0 else None]
613
+ if isinstance(value, list):
614
+ for i, item in enumerate(value):
615
+ if _deep_equal(item, needle):
616
+ return [i]
617
+ return [None]
618
+ return [None]
619
+
620
+ if name == "rindex":
621
+ if not args:
622
+ return [None]
623
+ needles = eval_fn(value, args[0], ctx)
624
+ needle = needles[0] if needles else None
625
+ if isinstance(value, str) and isinstance(needle, str):
626
+ idx = value.rfind(needle)
627
+ return [idx if idx >= 0 else None]
628
+ if isinstance(value, list):
629
+ for i in range(len(value) - 1, -1, -1):
630
+ if _deep_equal(value[i], needle):
631
+ return [i]
632
+ return [None]
633
+ return [None]
634
+
635
+ if name == "indices":
636
+ if not args:
637
+ return [[]]
638
+ needles = eval_fn(value, args[0], ctx)
639
+ needle = needles[0] if needles else None
640
+ result = []
641
+ if isinstance(value, str) and isinstance(needle, str):
642
+ idx = value.find(needle)
643
+ while idx != -1:
644
+ result.append(idx)
645
+ idx = value.find(needle, idx + 1)
646
+ elif isinstance(value, list):
647
+ for i, item in enumerate(value):
648
+ if _deep_equal(item, needle):
649
+ result.append(i)
650
+ return [result]
651
+
652
+ # Math functions
653
+ if name == "floor":
654
+ if isinstance(value, (int, float)):
655
+ return [math.floor(value)]
656
+ return [None]
657
+
658
+ if name == "ceil":
659
+ if isinstance(value, (int, float)):
660
+ return [math.ceil(value)]
661
+ return [None]
662
+
663
+ if name == "round":
664
+ if isinstance(value, (int, float)):
665
+ return [round(value)]
666
+ return [None]
667
+
668
+ if name == "sqrt":
669
+ if isinstance(value, (int, float)):
670
+ return [math.sqrt(value)]
671
+ return [None]
672
+
673
+ if name in ("fabs", "abs"):
674
+ if isinstance(value, (int, float)):
675
+ return [abs(value)]
676
+ return [None]
677
+
678
+ if name == "log":
679
+ if isinstance(value, (int, float)):
680
+ return [math.log(value)]
681
+ return [None]
682
+
683
+ if name == "log10":
684
+ if isinstance(value, (int, float)):
685
+ return [math.log10(value)]
686
+ return [None]
687
+
688
+ if name == "log2":
689
+ if isinstance(value, (int, float)):
690
+ return [math.log2(value)]
691
+ return [None]
692
+
693
+ if name == "exp":
694
+ if isinstance(value, (int, float)):
695
+ return [math.exp(value)]
696
+ return [None]
697
+
698
+ if name == "exp10":
699
+ if isinstance(value, (int, float)):
700
+ return [10**value]
701
+ return [None]
702
+
703
+ if name == "exp2":
704
+ if isinstance(value, (int, float)):
705
+ return [2**value]
706
+ return [None]
707
+
708
+ if name == "pow":
709
+ if not isinstance(value, (int, float)) or not args:
710
+ return [None]
711
+ exps = eval_fn(value, args[0], ctx)
712
+ exp = exps[0] if exps else 1
713
+ return [value**exp]
714
+
715
+ if name == "sin":
716
+ if isinstance(value, (int, float)):
717
+ return [math.sin(value)]
718
+ return [None]
719
+
720
+ if name == "cos":
721
+ if isinstance(value, (int, float)):
722
+ return [math.cos(value)]
723
+ return [None]
724
+
725
+ if name == "tan":
726
+ if isinstance(value, (int, float)):
727
+ return [math.tan(value)]
728
+ return [None]
729
+
730
+ if name == "asin":
731
+ if isinstance(value, (int, float)):
732
+ return [math.asin(value)]
733
+ return [None]
734
+
735
+ if name == "acos":
736
+ if isinstance(value, (int, float)):
737
+ return [math.acos(value)]
738
+ return [None]
739
+
740
+ if name == "atan":
741
+ if isinstance(value, (int, float)):
742
+ return [math.atan(value)]
743
+ return [None]
744
+
745
+ if name == "tostring":
746
+ if isinstance(value, str):
747
+ return [value]
748
+ return [json.dumps(value)]
749
+
750
+ if name == "tonumber":
751
+ if isinstance(value, (int, float)):
752
+ return [value]
753
+ if isinstance(value, str):
754
+ try:
755
+ return [float(value) if "." in value else int(value)]
756
+ except ValueError:
757
+ return [None]
758
+ return [None]
759
+
760
+ if name == "infinite":
761
+ return [not math.isfinite(value) if isinstance(value, (int, float)) else False]
762
+
763
+ if name == "nan":
764
+ return [math.isnan(value) if isinstance(value, (int, float)) else False]
765
+
766
+ if name == "isnan":
767
+ return [isinstance(value, (int, float)) and math.isnan(value)]
768
+
769
+ if name == "isinfinite":
770
+ return [isinstance(value, (int, float)) and not math.isfinite(value)]
771
+
772
+ if name == "isfinite":
773
+ return [isinstance(value, (int, float)) and math.isfinite(value)]
774
+
775
+ if name == "isnormal":
776
+ return [isinstance(value, (int, float)) and math.isfinite(value) and value != 0]
777
+
778
+ # Type filters
779
+ if name == "numbers":
780
+ # In Python, bool is a subclass of int, so we need to exclude bools
781
+ return [value] if isinstance(value, (int, float)) and not isinstance(value, bool) else []
782
+
783
+ if name == "strings":
784
+ return [value] if isinstance(value, str) else []
785
+
786
+ if name == "booleans":
787
+ return [value] if isinstance(value, bool) else []
788
+
789
+ if name == "nulls":
790
+ return [value] if value is None else []
791
+
792
+ if name == "arrays":
793
+ return [value] if isinstance(value, list) else []
794
+
795
+ if name == "objects":
796
+ return [value] if isinstance(value, dict) else []
797
+
798
+ if name == "iterables":
799
+ return [value] if isinstance(value, (list, dict)) else []
800
+
801
+ if name == "scalars":
802
+ return [value] if not isinstance(value, (list, dict)) else []
803
+
804
+ if name == "now":
805
+ import time
806
+
807
+ return [time.time()]
808
+
809
+ if name == "env":
810
+ return [ctx.env]
811
+
812
+ if name == "recurse":
813
+ if not args:
814
+ results = []
815
+ _walk_recurse(value, results)
816
+ return results
817
+ results = []
818
+ seen = set()
819
+
820
+ def walk(v: Any) -> None:
821
+ key = json.dumps(v, sort_keys=True) if isinstance(v, (dict, list)) else str(v)
822
+ if key in seen:
823
+ return
824
+ seen.add(key)
825
+ results.append(v)
826
+ nexts = eval_fn(v, args[0], ctx)
827
+ for n in nexts:
828
+ if n is not None:
829
+ walk(n)
830
+
831
+ walk(value)
832
+ return results
833
+
834
+ if name == "recurse_down":
835
+ return call_builtin(value, "recurse", args, ctx, eval_fn)
836
+
837
+ if name == "walk":
838
+ if not args:
839
+ return [value]
840
+ seen: set[int] = set()
841
+
842
+ def walk_fn(v: Any) -> Any:
843
+ if isinstance(v, (dict, list)):
844
+ obj_id = id(v)
845
+ if obj_id in seen:
846
+ return v
847
+ seen.add(obj_id)
848
+
849
+ if isinstance(v, list):
850
+ transformed = [walk_fn(item) for item in v]
851
+ elif isinstance(v, dict):
852
+ transformed = {k: walk_fn(val) for k, val in v.items()}
853
+ else:
854
+ transformed = v
855
+
856
+ results = eval_fn(transformed, args[0], ctx)
857
+ return results[0] if results else transformed
858
+
859
+ return [walk_fn(value)]
860
+
861
+ if name == "transpose":
862
+ if not isinstance(value, list):
863
+ return [None]
864
+ if not value:
865
+ return [[]]
866
+ max_len = max((len(row) if isinstance(row, list) else 0) for row in value)
867
+ result = []
868
+ for i in range(max_len):
869
+ row = [r[i] if isinstance(r, list) and i < len(r) else None for r in value]
870
+ result.append(row)
871
+ return [result]
872
+
873
+ if name == "ascii":
874
+ if isinstance(value, str) and value:
875
+ return [ord(value[0])]
876
+ return [None]
877
+
878
+ if name == "explode":
879
+ if isinstance(value, str):
880
+ return [[ord(c) for c in value]]
881
+ return [None]
882
+
883
+ if name == "implode":
884
+ if isinstance(value, list):
885
+ try:
886
+ return ["".join(chr(c) for c in value)]
887
+ except (TypeError, ValueError):
888
+ return [None]
889
+ return [None]
890
+
891
+ if name in ("tojson", "tojsonstream"):
892
+ return [json.dumps(value)]
893
+
894
+ if name == "fromjson":
895
+ if isinstance(value, str):
896
+ try:
897
+ return [json.loads(value)]
898
+ except json.JSONDecodeError:
899
+ return [None]
900
+ return [None]
901
+
902
+ if name == "limit":
903
+ if len(args) < 2:
904
+ return []
905
+ ns = eval_fn(value, args[0], ctx)
906
+ n = ns[0] if ns else 0
907
+ results = eval_fn(value, args[1], ctx)
908
+ return results[: int(n)]
909
+
910
+ if name == "until":
911
+ if len(args) < 2:
912
+ return [value]
913
+ current = value
914
+ max_iterations = ctx.limits.max_iterations
915
+ for _ in range(max_iterations):
916
+ conds = eval_fn(current, args[0], ctx)
917
+ if any(_is_truthy(c) for c in conds):
918
+ return [current]
919
+ nexts = eval_fn(current, args[1], ctx)
920
+ if not nexts:
921
+ return [current]
922
+ current = nexts[0]
923
+ raise ValueError(f"jq until: too many iterations ({max_iterations})")
924
+
925
+ if name == "while":
926
+ if len(args) < 2:
927
+ return [value]
928
+ results = []
929
+ current = value
930
+ max_iterations = ctx.limits.max_iterations
931
+ for _ in range(max_iterations):
932
+ conds = eval_fn(current, args[0], ctx)
933
+ if not any(_is_truthy(c) for c in conds):
934
+ break
935
+ results.append(current)
936
+ nexts = eval_fn(current, args[1], ctx)
937
+ if not nexts:
938
+ break
939
+ current = nexts[0]
940
+ return results
941
+
942
+ if name == "repeat":
943
+ if not args:
944
+ return [value]
945
+ results = []
946
+ current = value
947
+ max_iterations = ctx.limits.max_iterations
948
+ for _ in range(max_iterations):
949
+ results.append(current)
950
+ nexts = eval_fn(current, args[0], ctx)
951
+ if not nexts:
952
+ break
953
+ current = nexts[0]
954
+ return results
955
+
956
+ if name == "debug":
957
+ return [value]
958
+
959
+ if name == "input_line_number":
960
+ return [1]
961
+
962
+ # Format strings
963
+ if name == "@base64":
964
+ if isinstance(value, str):
965
+ return [base64.b64encode(value.encode("utf-8")).decode("utf-8")]
966
+ return [None]
967
+
968
+ if name == "@base64d":
969
+ if isinstance(value, str):
970
+ try:
971
+ return [base64.b64decode(value).decode("utf-8")]
972
+ except Exception:
973
+ return [None]
974
+ return [None]
975
+
976
+ if name == "@uri":
977
+ if isinstance(value, str):
978
+ return [uri_quote(value, safe="")]
979
+ return [None]
980
+
981
+ if name == "@csv":
982
+ if not isinstance(value, list):
983
+ return [None]
984
+ escaped = []
985
+ for v in value:
986
+ s = str(v) if v is not None else ""
987
+ if "," in s or '"' in s or "\n" in s:
988
+ escaped.append(f'"{s.replace(chr(34), chr(34) + chr(34))}"')
989
+ else:
990
+ escaped.append(s)
991
+ return [",".join(escaped)]
992
+
993
+ if name == "@tsv":
994
+ if not isinstance(value, list):
995
+ return [None]
996
+ escaped = []
997
+ for v in value:
998
+ s = str(v) if v is not None else ""
999
+ escaped.append(s.replace("\t", "\\t").replace("\n", "\\n"))
1000
+ return ["\t".join(escaped)]
1001
+
1002
+ if name == "@json":
1003
+ return [json.dumps(value)]
1004
+
1005
+ if name == "@html":
1006
+ if isinstance(value, str):
1007
+ return [
1008
+ value.replace("&", "&amp;")
1009
+ .replace("<", "&lt;")
1010
+ .replace(">", "&gt;")
1011
+ .replace('"', "&quot;")
1012
+ .replace("'", "&#39;")
1013
+ ]
1014
+ return [None]
1015
+
1016
+ if name == "@sh":
1017
+ if isinstance(value, str):
1018
+ return [f"'{value.replace(chr(39), chr(39) + chr(92) + chr(39) + chr(39))}'"]
1019
+ return [None]
1020
+
1021
+ if name == "@text":
1022
+ if isinstance(value, str):
1023
+ return [value]
1024
+ if value is None:
1025
+ return [""]
1026
+ return [str(value)]
1027
+
1028
+ # Navigation operators
1029
+ if name == "parent":
1030
+ if ctx.root is None or not ctx.current_path:
1031
+ return []
1032
+ path = ctx.current_path
1033
+ if not path:
1034
+ return []
1035
+ levels = 1
1036
+ if args:
1037
+ levels_vals = eval_fn(value, args[0], ctx)
1038
+ levels = levels_vals[0] if levels_vals else 1
1039
+
1040
+ if levels >= 0:
1041
+ if levels > len(path):
1042
+ return []
1043
+ parent_path = path[: len(path) - levels]
1044
+ else:
1045
+ target_len = -levels - 1
1046
+ if target_len >= len(path):
1047
+ return [value]
1048
+ parent_path = path[:target_len]
1049
+ return [_get_value_at_path(ctx.root, parent_path)]
1050
+
1051
+ if name == "parents":
1052
+ if ctx.root is None or not ctx.current_path:
1053
+ return [[]]
1054
+ path = ctx.current_path
1055
+ parents = []
1056
+ for i in range(len(path) - 1, -1, -1):
1057
+ parents.append(_get_value_at_path(ctx.root, path[:i]))
1058
+ return [parents]
1059
+
1060
+ if name == "root":
1061
+ return [ctx.root] if ctx.root is not None else []
1062
+
1063
+ raise ValueError(f"Unknown function: {name}")
1064
+
1065
+
1066
+ def _is_truthy(v: Any) -> bool:
1067
+ """Check if a value is truthy in jq terms."""
1068
+ return v is not None and v is not False
1069
+
1070
+
1071
+ def _deep_equal(a: Any, b: Any) -> bool:
1072
+ """Deep equality check."""
1073
+ return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
1074
+
1075
+
1076
+ def _jq_sort_key(v: Any) -> tuple[int, Any]:
1077
+ """Generate a sort key for jq-style sorting."""
1078
+ if v is None:
1079
+ return (0, 0)
1080
+ if isinstance(v, bool):
1081
+ return (1, int(v))
1082
+ if isinstance(v, (int, float)):
1083
+ return (2, v)
1084
+ if isinstance(v, str):
1085
+ return (3, v)
1086
+ if isinstance(v, list):
1087
+ return (4, json.dumps(v, sort_keys=True))
1088
+ if isinstance(v, dict):
1089
+ return (5, json.dumps(v, sort_keys=True))
1090
+ return (6, str(v))
1091
+
1092
+
1093
+ def _flatten(lst: list[Any], depth: int | None) -> list[Any]:
1094
+ """Flatten a list to a given depth."""
1095
+ if depth == 0:
1096
+ return lst
1097
+ result = []
1098
+ for item in lst:
1099
+ if isinstance(item, list):
1100
+ new_depth = None if depth is None else depth - 1
1101
+ result.extend(_flatten(item, new_depth))
1102
+ else:
1103
+ result.append(item)
1104
+ return result
1105
+
1106
+
1107
+ def _contains_deep(a: Any, b: Any) -> bool:
1108
+ """Check if a contains b (deep containment)."""
1109
+ if _deep_equal(a, b):
1110
+ return True
1111
+ if isinstance(a, list) and isinstance(b, list):
1112
+ return all(any(_contains_deep(a_item, b_item) for a_item in a) for b_item in b)
1113
+ if isinstance(a, dict) and isinstance(b, dict):
1114
+ return all(k in a and _contains_deep(a[k], v) for k, v in b.items())
1115
+ return False
1116
+
1117
+
1118
+ def _set_path(value: Any, path: list[str | int], new_val: Any) -> Any:
1119
+ """Set a value at a path."""
1120
+ if not path:
1121
+ return new_val
1122
+ head, *rest = path
1123
+ if isinstance(head, int):
1124
+ arr = list(value) if isinstance(value, list) else []
1125
+ while len(arr) <= head:
1126
+ arr.append(None)
1127
+ arr[head] = _set_path(arr[head], rest, new_val)
1128
+ return arr
1129
+ else:
1130
+ obj = dict(value) if isinstance(value, dict) else {}
1131
+ obj[head] = _set_path(obj.get(head), rest, new_val)
1132
+ return obj
1133
+
1134
+
1135
+ def _delete_path(value: Any, path: list[str | int]) -> Any:
1136
+ """Delete a value at a path."""
1137
+ if not path:
1138
+ return None
1139
+ if len(path) == 1:
1140
+ head = path[0]
1141
+ if isinstance(value, list) and isinstance(head, int):
1142
+ arr = list(value)
1143
+ if 0 <= head < len(arr):
1144
+ arr.pop(head)
1145
+ return arr
1146
+ if isinstance(value, dict) and isinstance(head, str):
1147
+ obj = dict(value)
1148
+ obj.pop(head, None)
1149
+ return obj
1150
+ return value
1151
+ head, *rest = path
1152
+ if isinstance(value, list) and isinstance(head, int):
1153
+ arr = list(value)
1154
+ if 0 <= head < len(arr):
1155
+ arr[head] = _delete_path(arr[head], rest)
1156
+ return arr
1157
+ if isinstance(value, dict) and isinstance(head, str):
1158
+ obj = dict(value)
1159
+ if head in obj:
1160
+ obj[head] = _delete_path(obj[head], rest)
1161
+ return obj
1162
+ return value
1163
+
1164
+
1165
+ def _get_all_paths(value: Any, current: list[str | int]) -> list[list[str | int]]:
1166
+ """Get all paths in a value."""
1167
+ paths = []
1168
+ if isinstance(value, dict):
1169
+ for k, v in value.items():
1170
+ new_path = current + [k]
1171
+ paths.append(new_path)
1172
+ paths.extend(_get_all_paths(v, new_path))
1173
+ elif isinstance(value, list):
1174
+ for i, v in enumerate(value):
1175
+ new_path = current + [i]
1176
+ paths.append(new_path)
1177
+ paths.extend(_get_all_paths(v, new_path))
1178
+ return paths
1179
+
1180
+
1181
+ def _get_leaf_paths(value: Any, current: list[str | int]) -> list[list[str | int]]:
1182
+ """Get all leaf paths (paths to non-container values)."""
1183
+ paths = []
1184
+ if value is None or not isinstance(value, (dict, list)):
1185
+ return [current] if current else []
1186
+ if isinstance(value, dict):
1187
+ if not value:
1188
+ return [current] if current else []
1189
+ for k, v in value.items():
1190
+ paths.extend(_get_leaf_paths(v, current + [k]))
1191
+ elif isinstance(value, list):
1192
+ if not value:
1193
+ return [current] if current else []
1194
+ for i, v in enumerate(value):
1195
+ paths.extend(_get_leaf_paths(v, current + [i]))
1196
+ return paths
1197
+
1198
+
1199
+ def _get_value_at_path(value: Any, path: list[str | int]) -> Any:
1200
+ """Get the value at a path."""
1201
+ current = value
1202
+ for key in path:
1203
+ if isinstance(current, dict) and isinstance(key, str):
1204
+ current = current.get(key)
1205
+ elif isinstance(current, list) and isinstance(key, int):
1206
+ current = current[key] if 0 <= key < len(current) else None
1207
+ else:
1208
+ return None
1209
+ return current
1210
+
1211
+
1212
+ def _collect_paths(
1213
+ value: Any,
1214
+ expr: AstNode,
1215
+ ctx: EvalContext,
1216
+ eval_fn: EvalFunc,
1217
+ current_path: list[str | int],
1218
+ paths: list[list[str | int]],
1219
+ ) -> None:
1220
+ """Collect paths that match an expression."""
1221
+ results = eval_fn(value, expr, ctx)
1222
+ if results:
1223
+ paths.append(current_path)
1224
+
1225
+
1226
+ def _apply_del(value: Any, path_expr: AstNode, ctx: EvalContext, eval_fn: EvalFunc) -> Any:
1227
+ """Apply deletion at a path."""
1228
+ if path_expr.type == "Identity":
1229
+ return None
1230
+ if path_expr.type == "Field":
1231
+ field_node = path_expr
1232
+ if isinstance(value, dict):
1233
+ result = dict(value)
1234
+ result.pop(field_node.name, None)
1235
+ return result
1236
+ return value
1237
+ if path_expr.type == "Index":
1238
+ index_node = path_expr
1239
+ indices = eval_fn(value, index_node.index, ctx)
1240
+ idx = indices[0] if indices else None
1241
+ if isinstance(idx, int) and isinstance(value, list):
1242
+ arr = list(value)
1243
+ i = idx if idx >= 0 else len(arr) + idx
1244
+ if 0 <= i < len(arr):
1245
+ arr.pop(i)
1246
+ return arr
1247
+ if isinstance(idx, str) and isinstance(value, dict):
1248
+ result = dict(value)
1249
+ result.pop(idx, None)
1250
+ return result
1251
+ return value
1252
+ if path_expr.type == "Iterate":
1253
+ if isinstance(value, list):
1254
+ return []
1255
+ if isinstance(value, dict):
1256
+ return {}
1257
+ return value
1258
+ return value
1259
+
1260
+
1261
+ def _walk_recurse(value: Any, results: list[Any]) -> None:
1262
+ """Walk recursively through a value."""
1263
+ results.append(value)
1264
+ if isinstance(value, list):
1265
+ for item in value:
1266
+ _walk_recurse(item, results)
1267
+ elif isinstance(value, dict):
1268
+ for v in value.values():
1269
+ _walk_recurse(v, results)
1270
+
1271
+
1272
+ def _get_re_flags(flags: str) -> int:
1273
+ """Convert jq regex flag string to Python re flags."""
1274
+ result = 0
1275
+ if "i" in flags:
1276
+ result |= re.IGNORECASE
1277
+ if "m" in flags:
1278
+ result |= re.MULTILINE
1279
+ if "s" in flags:
1280
+ result |= re.DOTALL
1281
+ if "x" in flags:
1282
+ result |= re.VERBOSE
1283
+ return result