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,299 @@
1
+ """Expand and unexpand command implementations."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class ExpandCommand:
7
+ """The expand command - convert tabs to spaces."""
8
+
9
+ name = "expand"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the expand command."""
13
+ tab_width = 8
14
+ tab_stops: list[int] = []
15
+ initial_only = False
16
+ files: list[str] = []
17
+
18
+ i = 0
19
+ while i < len(args):
20
+ arg = args[i]
21
+ if arg == "--help":
22
+ return ExecResult(
23
+ stdout="Usage: expand [OPTION]... [FILE]...\nConvert tabs to spaces.\n",
24
+ stderr="",
25
+ exit_code=0,
26
+ )
27
+ elif arg in ("-i", "--initial"):
28
+ initial_only = True
29
+ elif arg == "-t" and i + 1 < len(args):
30
+ i += 1
31
+ tab_stops = self._parse_tab_stops(args[i])
32
+ if tab_stops:
33
+ tab_width = tab_stops[0]
34
+ elif arg.startswith("-t"):
35
+ tab_stops = self._parse_tab_stops(arg[2:])
36
+ if tab_stops:
37
+ tab_width = tab_stops[0]
38
+ elif arg.startswith("--tabs="):
39
+ tab_stops = self._parse_tab_stops(arg[7:])
40
+ if tab_stops:
41
+ tab_width = tab_stops[0]
42
+ elif arg == "--":
43
+ files.extend(args[i + 1:])
44
+ break
45
+ elif arg.startswith("-") and len(arg) > 1:
46
+ # Could be -N for tab width
47
+ try:
48
+ tab_width = int(arg[1:])
49
+ except ValueError:
50
+ return ExecResult(
51
+ stdout="",
52
+ stderr=f"expand: invalid option -- '{arg[1]}'\n",
53
+ exit_code=1,
54
+ )
55
+ else:
56
+ files.append(arg)
57
+ i += 1
58
+
59
+ if tab_width <= 0:
60
+ return ExecResult(
61
+ stdout="",
62
+ stderr="expand: tab size must be greater than 0\n",
63
+ exit_code=1,
64
+ )
65
+
66
+ # Read from stdin if no files
67
+ if not files:
68
+ content = ctx.stdin
69
+ result = self._expand_content(content, tab_width, tab_stops, initial_only)
70
+ return ExecResult(stdout=result, stderr="", exit_code=0)
71
+
72
+ stdout_parts = []
73
+ stderr = ""
74
+ exit_code = 0
75
+
76
+ for file in files:
77
+ try:
78
+ if file == "-":
79
+ content = ctx.stdin
80
+ else:
81
+ path = ctx.fs.resolve_path(ctx.cwd, file)
82
+ content = await ctx.fs.read_file(path)
83
+
84
+ result = self._expand_content(content, tab_width, tab_stops, initial_only)
85
+ stdout_parts.append(result)
86
+
87
+ except FileNotFoundError:
88
+ stderr += f"expand: {file}: No such file or directory\n"
89
+ exit_code = 1
90
+
91
+ return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
92
+
93
+ def _parse_tab_stops(self, s: str) -> list[int]:
94
+ """Parse tab stop specification."""
95
+ if not s:
96
+ return []
97
+ try:
98
+ if "," in s:
99
+ return [int(x) for x in s.split(",") if x]
100
+ return [int(s)]
101
+ except ValueError:
102
+ return []
103
+
104
+ def _expand_content(
105
+ self, content: str, tab_width: int, tab_stops: list[int], initial_only: bool
106
+ ) -> str:
107
+ """Expand tabs in content."""
108
+ lines = content.split("\n")
109
+ result_lines = []
110
+
111
+ for line in lines:
112
+ result_lines.append(self._expand_line(line, tab_width, tab_stops, initial_only))
113
+
114
+ return "\n".join(result_lines)
115
+
116
+ def _expand_line(
117
+ self, line: str, tab_width: int, tab_stops: list[int], initial_only: bool
118
+ ) -> str:
119
+ """Expand tabs in a single line."""
120
+ result = []
121
+ column = 0
122
+ in_initial = True
123
+
124
+ for char in line:
125
+ if char == "\t":
126
+ if initial_only and not in_initial:
127
+ result.append(char)
128
+ column += 1
129
+ else:
130
+ # Calculate spaces needed to reach next tab stop
131
+ if tab_stops and len(tab_stops) > 1:
132
+ # Find next tab stop
133
+ next_stop = None
134
+ for stop in tab_stops:
135
+ if stop > column:
136
+ next_stop = stop
137
+ break
138
+ if next_stop is None:
139
+ # Use last interval
140
+ interval = tab_stops[-1] - tab_stops[-2] if len(tab_stops) > 1 else tab_width
141
+ next_stop = column + interval - (column - tab_stops[-1]) % interval
142
+ spaces = next_stop - column
143
+ else:
144
+ spaces = tab_width - (column % tab_width)
145
+ result.append(" " * spaces)
146
+ column += spaces
147
+ else:
148
+ if char != " ":
149
+ in_initial = False
150
+ result.append(char)
151
+ column += 1
152
+
153
+ return "".join(result)
154
+
155
+
156
+ class UnexpandCommand:
157
+ """The unexpand command - convert spaces to tabs."""
158
+
159
+ name = "unexpand"
160
+
161
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
162
+ """Execute the unexpand command."""
163
+ tab_width = 8
164
+ all_spaces = False
165
+ files: list[str] = []
166
+
167
+ i = 0
168
+ while i < len(args):
169
+ arg = args[i]
170
+ if arg == "--help":
171
+ return ExecResult(
172
+ stdout="Usage: unexpand [OPTION]... [FILE]...\nConvert spaces to tabs.\n",
173
+ stderr="",
174
+ exit_code=0,
175
+ )
176
+ elif arg in ("-a", "--all"):
177
+ all_spaces = True
178
+ elif arg == "-t" and i + 1 < len(args):
179
+ i += 1
180
+ try:
181
+ tab_width = int(args[i])
182
+ except ValueError:
183
+ return ExecResult(
184
+ stdout="",
185
+ stderr=f"unexpand: invalid tab size: '{args[i]}'\n",
186
+ exit_code=1,
187
+ )
188
+ elif arg.startswith("-t"):
189
+ try:
190
+ tab_width = int(arg[2:])
191
+ except ValueError:
192
+ return ExecResult(
193
+ stdout="",
194
+ stderr=f"unexpand: invalid tab size: '{arg[2:]}'\n",
195
+ exit_code=1,
196
+ )
197
+ elif arg.startswith("--tabs="):
198
+ try:
199
+ tab_width = int(arg[7:])
200
+ except ValueError:
201
+ return ExecResult(
202
+ stdout="",
203
+ stderr=f"unexpand: invalid tab size: '{arg[7:]}'\n",
204
+ exit_code=1,
205
+ )
206
+ elif arg == "--":
207
+ files.extend(args[i + 1:])
208
+ break
209
+ elif arg.startswith("-") and len(arg) > 1:
210
+ return ExecResult(
211
+ stdout="",
212
+ stderr=f"unexpand: invalid option -- '{arg[1]}'\n",
213
+ exit_code=1,
214
+ )
215
+ else:
216
+ files.append(arg)
217
+ i += 1
218
+
219
+ if tab_width <= 0:
220
+ return ExecResult(
221
+ stdout="",
222
+ stderr="unexpand: tab size must be greater than 0\n",
223
+ exit_code=1,
224
+ )
225
+
226
+ # Read from stdin if no files
227
+ if not files:
228
+ content = ctx.stdin
229
+ result = self._unexpand_content(content, tab_width, all_spaces)
230
+ return ExecResult(stdout=result, stderr="", exit_code=0)
231
+
232
+ stdout_parts = []
233
+ stderr = ""
234
+ exit_code = 0
235
+
236
+ for file in files:
237
+ try:
238
+ if file == "-":
239
+ content = ctx.stdin
240
+ else:
241
+ path = ctx.fs.resolve_path(ctx.cwd, file)
242
+ content = await ctx.fs.read_file(path)
243
+
244
+ result = self._unexpand_content(content, tab_width, all_spaces)
245
+ stdout_parts.append(result)
246
+
247
+ except FileNotFoundError:
248
+ stderr += f"unexpand: {file}: No such file or directory\n"
249
+ exit_code = 1
250
+
251
+ return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
252
+
253
+ def _unexpand_content(self, content: str, tab_width: int, all_spaces: bool) -> str:
254
+ """Unexpand spaces in content."""
255
+ lines = content.split("\n")
256
+ result_lines = []
257
+
258
+ for line in lines:
259
+ result_lines.append(self._unexpand_line(line, tab_width, all_spaces))
260
+
261
+ return "\n".join(result_lines)
262
+
263
+ def _unexpand_line(self, line: str, tab_width: int, all_spaces: bool) -> str:
264
+ """Unexpand spaces in a single line."""
265
+ if not line:
266
+ return line
267
+
268
+ result = []
269
+ space_count = 0
270
+ column = 0
271
+ in_leading = True
272
+
273
+ for char in line:
274
+ if char == " ":
275
+ space_count += 1
276
+ column += 1
277
+
278
+ # Check if we've reached a tab stop
279
+ if column % tab_width == 0:
280
+ if in_leading or all_spaces:
281
+ result.append("\t")
282
+ else:
283
+ result.append(" " * space_count)
284
+ space_count = 0
285
+ else:
286
+ # Flush pending spaces
287
+ if space_count > 0:
288
+ result.append(" " * space_count)
289
+ space_count = 0
290
+ result.append(char)
291
+ column += 1
292
+ if char != " ":
293
+ in_leading = False
294
+
295
+ # Flush any remaining spaces
296
+ if space_count > 0:
297
+ result.append(" " * space_count)
298
+
299
+ return "".join(result)
@@ -0,0 +1,5 @@
1
+ """Expr command implementation."""
2
+
3
+ from .expr import ExprCommand
4
+
5
+ __all__ = ["ExprCommand"]
@@ -0,0 +1,273 @@
1
+ """Expr command implementation.
2
+
3
+ Usage: expr EXPRESSION
4
+
5
+ Print the value of EXPRESSION to standard output.
6
+
7
+ EXPRESSION may be:
8
+ ARG1 | ARG2 ARG1 if it is neither null nor 0, otherwise ARG2
9
+ ARG1 & ARG2 ARG1 if neither argument is null or 0, otherwise 0
10
+ ARG1 < ARG2 ARG1 is less than ARG2
11
+ ARG1 <= ARG2 ARG1 is less than or equal to ARG2
12
+ ARG1 = ARG2 ARG1 is equal to ARG2
13
+ ARG1 != ARG2 ARG1 is not equal to ARG2
14
+ ARG1 >= ARG2 ARG1 is greater than or equal to ARG2
15
+ ARG1 > ARG2 ARG1 is greater than ARG2
16
+ ARG1 + ARG2 arithmetic sum of ARG1 and ARG2
17
+ ARG1 - ARG2 arithmetic difference of ARG1 and ARG2
18
+ ARG1 * ARG2 arithmetic product of ARG1 and ARG2
19
+ ARG1 / ARG2 arithmetic quotient of ARG1 divided by ARG2
20
+ ARG1 % ARG2 arithmetic remainder of ARG1 divided by ARG2
21
+ STRING : REGEXP anchored pattern match of REGEXP in STRING
22
+ match STRING REGEXP same as STRING : REGEXP
23
+ substr STRING POS LENGTH substring of STRING, POS counted from 1
24
+ index STRING CHARS index in STRING where any CHARS is found, or 0
25
+ length STRING length of STRING
26
+ + TOKEN interpret TOKEN as a string
27
+ ( EXPRESSION ) value of EXPRESSION
28
+ """
29
+
30
+ import re
31
+ from ...types import CommandContext, ExecResult
32
+
33
+
34
+ class ExprCommand:
35
+ """The expr command."""
36
+
37
+ name = "expr"
38
+
39
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
40
+ """Execute the expr command."""
41
+ if not args:
42
+ return ExecResult(
43
+ stdout="",
44
+ stderr="expr: missing operand\n",
45
+ exit_code=2,
46
+ )
47
+
48
+ try:
49
+ result, _ = self._evaluate(args, 0)
50
+ # Exit code is 1 if result is empty or 0
51
+ exit_code = 1 if result == "" or result == "0" else 0
52
+ return ExecResult(stdout=str(result) + "\n", stderr="", exit_code=exit_code)
53
+ except ValueError as e:
54
+ return ExecResult(stdout="", stderr=f"expr: {e}\n", exit_code=2)
55
+ except ZeroDivisionError:
56
+ return ExecResult(stdout="", stderr="expr: division by zero\n", exit_code=2)
57
+
58
+ def _evaluate(self, args: list[str], pos: int) -> tuple[str, int]:
59
+ """Evaluate expression starting at position."""
60
+ return self._parse_or(args, pos)
61
+
62
+ def _parse_or(self, args: list[str], pos: int) -> tuple[str, int]:
63
+ """Parse OR expression: ARG1 | ARG2."""
64
+ left, pos = self._parse_and(args, pos)
65
+
66
+ while pos < len(args) and args[pos] == "|":
67
+ pos += 1
68
+ right, pos = self._parse_and(args, pos)
69
+ if left != "" and left != "0":
70
+ pass # keep left
71
+ else:
72
+ left = right
73
+
74
+ return left, pos
75
+
76
+ def _parse_and(self, args: list[str], pos: int) -> tuple[str, int]:
77
+ """Parse AND expression: ARG1 & ARG2."""
78
+ left, pos = self._parse_comparison(args, pos)
79
+
80
+ while pos < len(args) and args[pos] == "&":
81
+ pos += 1
82
+ right, pos = self._parse_comparison(args, pos)
83
+ if (left == "" or left == "0") or (right == "" or right == "0"):
84
+ left = "0"
85
+ # else keep left
86
+
87
+ return left, pos
88
+
89
+ def _parse_comparison(self, args: list[str], pos: int) -> tuple[str, int]:
90
+ """Parse comparison: <, <=, =, !=, >=, >."""
91
+ left, pos = self._parse_additive(args, pos)
92
+
93
+ if pos < len(args) and args[pos] in ("<", "<=", "=", "!=", ">=", ">"):
94
+ op = args[pos]
95
+ pos += 1
96
+ right, pos = self._parse_additive(args, pos)
97
+
98
+ # Try numeric comparison first
99
+ try:
100
+ l_num = int(left)
101
+ r_num = int(right)
102
+ if op == "<":
103
+ result = l_num < r_num
104
+ elif op == "<=":
105
+ result = l_num <= r_num
106
+ elif op == "=":
107
+ result = l_num == r_num
108
+ elif op == "!=":
109
+ result = l_num != r_num
110
+ elif op == ">=":
111
+ result = l_num >= r_num
112
+ elif op == ">":
113
+ result = l_num > r_num
114
+ except ValueError:
115
+ # String comparison
116
+ if op == "<":
117
+ result = left < right
118
+ elif op == "<=":
119
+ result = left <= right
120
+ elif op == "=":
121
+ result = left == right
122
+ elif op == "!=":
123
+ result = left != right
124
+ elif op == ">=":
125
+ result = left >= right
126
+ elif op == ">":
127
+ result = left > right
128
+
129
+ left = "1" if result else "0"
130
+
131
+ return left, pos
132
+
133
+ def _parse_additive(self, args: list[str], pos: int) -> tuple[str, int]:
134
+ """Parse additive: + and -."""
135
+ left, pos = self._parse_multiplicative(args, pos)
136
+
137
+ while pos < len(args) and args[pos] in ("+", "-"):
138
+ op = args[pos]
139
+ pos += 1
140
+ right, pos = self._parse_multiplicative(args, pos)
141
+
142
+ try:
143
+ l_num = int(left)
144
+ r_num = int(right)
145
+ if op == "+":
146
+ left = str(l_num + r_num)
147
+ else:
148
+ left = str(l_num - r_num)
149
+ except ValueError:
150
+ raise ValueError("non-integer argument")
151
+
152
+ return left, pos
153
+
154
+ def _parse_multiplicative(self, args: list[str], pos: int) -> tuple[str, int]:
155
+ """Parse multiplicative: *, /, %."""
156
+ left, pos = self._parse_match(args, pos)
157
+
158
+ while pos < len(args) and args[pos] in ("*", "/", "%"):
159
+ op = args[pos]
160
+ pos += 1
161
+ right, pos = self._parse_match(args, pos)
162
+
163
+ try:
164
+ l_num = int(left)
165
+ r_num = int(right)
166
+ if op == "*":
167
+ left = str(l_num * r_num)
168
+ elif op == "/":
169
+ left = str(l_num // r_num)
170
+ else:
171
+ left = str(l_num % r_num)
172
+ except ValueError:
173
+ raise ValueError("non-integer argument")
174
+
175
+ return left, pos
176
+
177
+ def _parse_match(self, args: list[str], pos: int) -> tuple[str, int]:
178
+ """Parse match expression: STRING : REGEXP."""
179
+ left, pos = self._parse_primary(args, pos)
180
+
181
+ if pos < len(args) and args[pos] == ":":
182
+ pos += 1
183
+ right, pos = self._parse_primary(args, pos)
184
+ # Anchored match at start
185
+ pattern = "^(" + right + ")"
186
+ try:
187
+ match = re.match(pattern, left)
188
+ if match:
189
+ if match.groups():
190
+ left = match.group(1)
191
+ else:
192
+ left = str(len(match.group(0)))
193
+ else:
194
+ left = ""
195
+ except re.error as e:
196
+ raise ValueError(f"invalid pattern: {e}")
197
+
198
+ return left, pos
199
+
200
+ def _parse_primary(self, args: list[str], pos: int) -> tuple[str, int]:
201
+ """Parse primary expression."""
202
+ if pos >= len(args):
203
+ raise ValueError("missing operand")
204
+
205
+ token = args[pos]
206
+
207
+ # Parentheses
208
+ if token == "(":
209
+ pos += 1
210
+ result, pos = self._evaluate(args, pos)
211
+ if pos >= len(args) or args[pos] != ")":
212
+ raise ValueError("unmatched '('")
213
+ pos += 1
214
+ return result, pos
215
+
216
+ # Built-in functions
217
+ if token == "match" and pos + 2 < len(args):
218
+ pos += 1
219
+ string = args[pos]
220
+ pos += 1
221
+ pattern = args[pos]
222
+ pos += 1
223
+ # Anchored match
224
+ try:
225
+ match = re.match("^(" + pattern + ")", string)
226
+ if match:
227
+ if match.groups():
228
+ return match.group(1), pos
229
+ return str(len(match.group(0))), pos
230
+ return "", pos
231
+ except re.error as e:
232
+ raise ValueError(f"invalid pattern: {e}")
233
+
234
+ if token == "substr" and pos + 3 < len(args):
235
+ pos += 1
236
+ string = args[pos]
237
+ pos += 1
238
+ try:
239
+ start = int(args[pos]) - 1 # 1-indexed
240
+ pos += 1
241
+ length = int(args[pos])
242
+ pos += 1
243
+ if start < 0:
244
+ start = 0
245
+ return string[start:start + length], pos
246
+ except ValueError:
247
+ raise ValueError("non-integer argument")
248
+
249
+ if token == "index" and pos + 2 < len(args):
250
+ pos += 1
251
+ string = args[pos]
252
+ pos += 1
253
+ chars = args[pos]
254
+ pos += 1
255
+ for i, c in enumerate(string):
256
+ if c in chars:
257
+ return str(i + 1), pos # 1-indexed
258
+ return "0", pos
259
+
260
+ if token == "length" and pos + 1 < len(args):
261
+ pos += 1
262
+ string = args[pos]
263
+ pos += 1
264
+ return str(len(string)), pos
265
+
266
+ # Quote next token as string
267
+ if token == "+":
268
+ pos += 1
269
+ if pos >= len(args):
270
+ raise ValueError("missing operand after '+'")
271
+ return args[pos], pos + 1
272
+
273
+ return token, pos + 1
@@ -0,0 +1,5 @@
1
+ """File command."""
2
+
3
+ from .file import FileCommand
4
+
5
+ __all__ = ["FileCommand"]