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,411 @@
1
+ """Sort command implementation.
2
+
3
+ Usage: sort [OPTION]... [FILE]...
4
+
5
+ Write sorted concatenation of all FILE(s) to standard output.
6
+
7
+ Options:
8
+ -b, --ignore-leading-blanks ignore leading blanks
9
+ -f, --ignore-case fold lower case to upper case characters
10
+ -n, --numeric-sort compare according to string numerical value
11
+ -r, --reverse reverse the result of comparisons
12
+ -u, --unique output only the first of an equal run
13
+ -t, --field-separator=SEP use SEP instead of non-blank to blank transition
14
+ -k, --key=KEYDEF sort via a key; KEYDEF gives location and type
15
+ -o, --output=FILE write result to FILE instead of standard output
16
+ -s, --stable stabilize sort by disabling last-resort comparison
17
+
18
+ KEYDEF is F[.C][OPTS][,F[.C][OPTS]] for start and stop position.
19
+ F is field number, C is character position (both 1-indexed).
20
+ OPTS is one or more single-letter ordering options [bfnr].
21
+ """
22
+
23
+ import re
24
+ from ...types import CommandContext, ExecResult
25
+
26
+
27
+ class SortCommand:
28
+ """The sort command."""
29
+
30
+ name = "sort"
31
+
32
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
33
+ """Execute the sort command."""
34
+ ignore_blanks = False
35
+ ignore_case = False
36
+ numeric = False
37
+ reverse = False
38
+ unique = False
39
+ separator = None
40
+ keys: list[dict] = []
41
+ output_file = None
42
+ stable = False
43
+ check_sorted = False
44
+ human_numeric = False
45
+ version_sort = False
46
+ month_sort = False
47
+ dictionary_order = False
48
+ files: list[str] = []
49
+
50
+ # Parse arguments
51
+ i = 0
52
+ while i < len(args):
53
+ arg = args[i]
54
+ if arg == "--":
55
+ files.extend(args[i + 1:])
56
+ break
57
+ elif arg.startswith("--"):
58
+ if arg == "--ignore-leading-blanks":
59
+ ignore_blanks = True
60
+ elif arg == "--ignore-case":
61
+ ignore_case = True
62
+ elif arg == "--numeric-sort":
63
+ numeric = True
64
+ elif arg == "--reverse":
65
+ reverse = True
66
+ elif arg == "--unique":
67
+ unique = True
68
+ elif arg == "--stable":
69
+ stable = True
70
+ elif arg.startswith("--field-separator="):
71
+ separator = arg[18:]
72
+ elif arg.startswith("--key="):
73
+ key = self._parse_key(arg[6:])
74
+ if key is None:
75
+ return ExecResult(
76
+ stdout="",
77
+ stderr=f"sort: invalid key specification: '{arg[6:]}'\n",
78
+ exit_code=1,
79
+ )
80
+ keys.append(key)
81
+ elif arg.startswith("--output="):
82
+ output_file = arg[9:]
83
+ else:
84
+ return ExecResult(
85
+ stdout="",
86
+ stderr=f"sort: unrecognized option '{arg}'\n",
87
+ exit_code=1,
88
+ )
89
+ elif arg.startswith("-") and arg != "-":
90
+ j = 1
91
+ while j < len(arg):
92
+ c = arg[j]
93
+ if c == "b":
94
+ ignore_blanks = True
95
+ elif c == "f":
96
+ ignore_case = True
97
+ elif c == "n":
98
+ numeric = True
99
+ elif c == "r":
100
+ reverse = True
101
+ elif c == "u":
102
+ unique = True
103
+ elif c == "s":
104
+ stable = True
105
+ elif c == "c":
106
+ check_sorted = True
107
+ elif c == "h":
108
+ human_numeric = True
109
+ elif c == "V":
110
+ version_sort = True
111
+ elif c == "M":
112
+ month_sort = True
113
+ elif c == "d":
114
+ dictionary_order = True
115
+ elif c == "t":
116
+ # -t requires a value
117
+ if j + 1 < len(arg):
118
+ separator = arg[j + 1:]
119
+ break
120
+ elif i + 1 < len(args):
121
+ i += 1
122
+ separator = args[i]
123
+ break
124
+ else:
125
+ return ExecResult(
126
+ stdout="",
127
+ stderr="sort: option requires an argument -- 't'\n",
128
+ exit_code=1,
129
+ )
130
+ elif c == "k":
131
+ # -k requires a value
132
+ if j + 1 < len(arg):
133
+ key_spec = arg[j + 1:]
134
+ elif i + 1 < len(args):
135
+ i += 1
136
+ key_spec = args[i]
137
+ else:
138
+ return ExecResult(
139
+ stdout="",
140
+ stderr="sort: option requires an argument -- 'k'\n",
141
+ exit_code=1,
142
+ )
143
+ key = self._parse_key(key_spec)
144
+ if key is None:
145
+ return ExecResult(
146
+ stdout="",
147
+ stderr=f"sort: invalid key specification: '{key_spec}'\n",
148
+ exit_code=1,
149
+ )
150
+ keys.append(key)
151
+ break
152
+ elif c == "o":
153
+ # -o requires a value
154
+ if j + 1 < len(arg):
155
+ output_file = arg[j + 1:]
156
+ break
157
+ elif i + 1 < len(args):
158
+ i += 1
159
+ output_file = args[i]
160
+ break
161
+ else:
162
+ return ExecResult(
163
+ stdout="",
164
+ stderr="sort: option requires an argument -- 'o'\n",
165
+ exit_code=1,
166
+ )
167
+ else:
168
+ return ExecResult(
169
+ stdout="",
170
+ stderr=f"sort: invalid option -- '{c}'\n",
171
+ exit_code=1,
172
+ )
173
+ j += 1
174
+ else:
175
+ files.append(arg)
176
+ i += 1
177
+
178
+ # Default to stdin
179
+ if not files:
180
+ files = ["-"]
181
+
182
+ # Read all lines from all files
183
+ all_lines: list[str] = []
184
+ stderr = ""
185
+ exit_code = 0
186
+
187
+ for f in files:
188
+ try:
189
+ if f == "-":
190
+ content = ctx.stdin
191
+ else:
192
+ path = ctx.fs.resolve_path(ctx.cwd, f)
193
+ content = await ctx.fs.read_file(path)
194
+
195
+ lines = content.split("\n")
196
+ # Remove trailing empty line if present
197
+ if lines and lines[-1] == "":
198
+ lines = lines[:-1]
199
+ all_lines.extend(lines)
200
+
201
+ except FileNotFoundError:
202
+ stderr += f"sort: {f}: No such file or directory\n"
203
+ exit_code = 1
204
+
205
+ if exit_code != 0:
206
+ return ExecResult(stdout="", stderr=stderr, exit_code=exit_code)
207
+
208
+ # Create sort key function
209
+ def make_key(line: str):
210
+ if version_sort:
211
+ return self._version_key(line)
212
+ if month_sort:
213
+ return self._month_key(line)
214
+ if human_numeric:
215
+ return self._human_numeric_key(line)
216
+ if keys:
217
+ key_values = []
218
+ for key in keys:
219
+ val = self._extract_key(line, key, separator)
220
+ key_values.append(self._make_comparable(val, key))
221
+ return tuple(key_values)
222
+ else:
223
+ return self._make_comparable(
224
+ line,
225
+ {
226
+ "ignore_blanks": ignore_blanks,
227
+ "ignore_case": ignore_case,
228
+ "numeric": numeric,
229
+ "dictionary_order": dictionary_order,
230
+ },
231
+ )
232
+
233
+ # Check sorted mode
234
+ if check_sorted:
235
+ for i in range(1, len(all_lines)):
236
+ prev_key = make_key(all_lines[i - 1])
237
+ curr_key = make_key(all_lines[i])
238
+ if reverse:
239
+ is_sorted = prev_key >= curr_key
240
+ else:
241
+ is_sorted = prev_key <= curr_key
242
+ if not is_sorted:
243
+ return ExecResult(
244
+ stdout="",
245
+ stderr=f"sort: {files[0]}:{i + 1}: disorder: {all_lines[i]}\n",
246
+ exit_code=1,
247
+ )
248
+ return ExecResult(stdout="", stderr="", exit_code=0)
249
+
250
+ # Sort
251
+ try:
252
+ sorted_lines = sorted(all_lines, key=make_key, reverse=reverse)
253
+ except Exception:
254
+ # Fallback to string sort
255
+ sorted_lines = sorted(all_lines, reverse=reverse)
256
+
257
+ # Apply unique
258
+ if unique:
259
+ unique_lines = []
260
+ seen_keys = set()
261
+ for line in sorted_lines:
262
+ key = make_key(line)
263
+ if key not in seen_keys:
264
+ seen_keys.add(key)
265
+ unique_lines.append(line)
266
+ sorted_lines = unique_lines
267
+
268
+ # Generate output
269
+ stdout = "\n".join(sorted_lines)
270
+ if sorted_lines:
271
+ stdout += "\n"
272
+
273
+ # Write to output file if specified
274
+ if output_file:
275
+ try:
276
+ path = ctx.fs.resolve_path(ctx.cwd, output_file)
277
+ await ctx.fs.write_file(path, stdout)
278
+ return ExecResult(stdout="", stderr=stderr, exit_code=exit_code)
279
+ except Exception as e:
280
+ return ExecResult(
281
+ stdout="",
282
+ stderr=f"sort: {output_file}: {e}\n",
283
+ exit_code=1,
284
+ )
285
+
286
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
287
+
288
+ def _parse_key(self, spec: str) -> dict | None:
289
+ """Parse a key specification like '2,2' or '1.3,1.5n'."""
290
+ # Pattern: F[.C][OPTS][,F[.C][OPTS]]
291
+ pattern = r"^(\d+)(?:\.(\d+))?([bfnr]*)?(?:,(\d+)(?:\.(\d+))?([bfnr]*)?)?$"
292
+ match = re.match(pattern, spec)
293
+ if not match:
294
+ return None
295
+
296
+ key = {
297
+ "start_field": int(match.group(1)),
298
+ "start_char": int(match.group(2)) if match.group(2) else 1,
299
+ "end_field": int(match.group(4)) if match.group(4) else None,
300
+ "end_char": int(match.group(5)) if match.group(5) else None,
301
+ "ignore_blanks": "b" in (match.group(3) or "") or "b" in (match.group(6) or ""),
302
+ "ignore_case": "f" in (match.group(3) or "") or "f" in (match.group(6) or ""),
303
+ "numeric": "n" in (match.group(3) or "") or "n" in (match.group(6) or ""),
304
+ "reverse": "r" in (match.group(3) or "") or "r" in (match.group(6) or ""),
305
+ }
306
+ return key
307
+
308
+ def _extract_key(self, line: str, key: dict, separator: str | None) -> str:
309
+ """Extract the key portion from a line."""
310
+ if separator:
311
+ fields = line.split(separator)
312
+ else:
313
+ # Split on whitespace runs
314
+ fields = line.split()
315
+
316
+ start_field = key["start_field"] - 1 # 0-indexed
317
+ start_char = key["start_char"] - 1 # 0-indexed
318
+ end_field = key.get("end_field")
319
+ end_char = key.get("end_char")
320
+
321
+ if start_field >= len(fields):
322
+ return ""
323
+
324
+ if end_field is None:
325
+ # Just the start field from start_char
326
+ field_content = fields[start_field] if start_field < len(fields) else ""
327
+ return field_content[start_char:]
328
+ else:
329
+ end_field -= 1 # 0-indexed
330
+ if end_field >= len(fields):
331
+ end_field = len(fields) - 1
332
+
333
+ # Extract from start to end field
334
+ parts = []
335
+ for i in range(start_field, end_field + 1):
336
+ if i >= len(fields):
337
+ break
338
+ if i == start_field:
339
+ parts.append(fields[i][start_char:])
340
+ elif i == end_field and end_char:
341
+ parts.append(fields[i][:end_char])
342
+ else:
343
+ parts.append(fields[i])
344
+
345
+ return (separator or " ").join(parts)
346
+
347
+ def _make_comparable(self, val: str, opts: dict) -> tuple:
348
+ """Make a value comparable based on options."""
349
+ if opts.get("ignore_blanks"):
350
+ val = val.lstrip()
351
+
352
+ # Dictionary order: keep only blanks and alphanumerics for comparison
353
+ if opts.get("dictionary_order"):
354
+ compare_val = ''.join(c for c in val if c.isalnum() or c.isspace())
355
+ else:
356
+ compare_val = val
357
+
358
+ if opts.get("ignore_case"):
359
+ compare_val = compare_val.lower()
360
+
361
+ if opts.get("numeric"):
362
+ # Try to extract leading number
363
+ match = re.match(r"^\s*(-?\d+(?:\.\d+)?)", compare_val)
364
+ if match:
365
+ try:
366
+ num = float(match.group(1))
367
+ return (0, num, compare_val)
368
+ except ValueError:
369
+ pass
370
+ # Non-numeric sorts before any number
371
+ return (1, 0, compare_val)
372
+
373
+ return (0, compare_val)
374
+
375
+ def _version_key(self, val: str) -> tuple:
376
+ """Create a sort key for version sorting (v1.2.10 style)."""
377
+ # Extract version components
378
+ parts = []
379
+ for part in re.split(r"(\d+)", val):
380
+ if part.isdigit():
381
+ parts.append((0, int(part)))
382
+ elif part:
383
+ parts.append((1, part))
384
+ return tuple(parts)
385
+
386
+ def _month_key(self, val: str) -> tuple:
387
+ """Create a sort key for month sorting."""
388
+ months = {
389
+ "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
390
+ "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12,
391
+ "january": 1, "february": 2, "march": 3, "april": 4,
392
+ "june": 6, "july": 7, "august": 8, "september": 9,
393
+ "october": 10, "november": 11, "december": 12,
394
+ }
395
+ val_lower = val.strip().lower()[:3]
396
+ month_num = months.get(val_lower, 0)
397
+ return (month_num, val)
398
+
399
+ def _human_numeric_key(self, val: str) -> tuple:
400
+ """Create a sort key for human-readable numeric sorting (1K, 1M, 1G)."""
401
+ suffixes = {"": 1, "k": 1024, "m": 1024**2, "g": 1024**3, "t": 1024**4}
402
+ match = re.match(r"^\s*(-?\d+(?:\.\d+)?)\s*([kmgt]?)i?\s*$", val.strip(), re.IGNORECASE)
403
+ if match:
404
+ try:
405
+ num = float(match.group(1))
406
+ suffix = match.group(2).lower()
407
+ multiplier = suffixes.get(suffix, 1)
408
+ return (0, num * multiplier)
409
+ except ValueError:
410
+ pass
411
+ return (1, val)
@@ -0,0 +1,5 @@
1
+ """Split command."""
2
+
3
+ from .split import SplitCommand
4
+
5
+ __all__ = ["SplitCommand"]
@@ -0,0 +1,237 @@
1
+ """Split command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class SplitCommand:
7
+ """The split command - split a file into pieces."""
8
+
9
+ name = "split"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the split command."""
13
+ lines_per_file = 1000
14
+ bytes_per_file = None
15
+ num_chunks = None
16
+ numeric_suffix = False
17
+ suffix_length = 2
18
+ prefix = "x"
19
+ file_path = None
20
+
21
+ i = 0
22
+ while i < len(args):
23
+ arg = args[i]
24
+ if arg == "--help":
25
+ return ExecResult(
26
+ stdout="Usage: split [OPTION]... [FILE [PREFIX]]\nSplit a file into pieces.\n",
27
+ stderr="",
28
+ exit_code=0,
29
+ )
30
+ elif arg == "-l" and i + 1 < len(args):
31
+ i += 1
32
+ try:
33
+ lines_per_file = int(args[i])
34
+ except ValueError:
35
+ return ExecResult(
36
+ stdout="",
37
+ stderr=f"split: invalid number of lines: '{args[i]}'\n",
38
+ exit_code=1,
39
+ )
40
+ elif arg.startswith("-l"):
41
+ try:
42
+ lines_per_file = int(arg[2:])
43
+ except ValueError:
44
+ return ExecResult(
45
+ stdout="",
46
+ stderr=f"split: invalid number of lines: '{arg[2:]}'\n",
47
+ exit_code=1,
48
+ )
49
+ elif arg == "-b" and i + 1 < len(args):
50
+ i += 1
51
+ bytes_per_file = self._parse_size(args[i])
52
+ if bytes_per_file is None:
53
+ return ExecResult(
54
+ stdout="",
55
+ stderr=f"split: invalid number of bytes: '{args[i]}'\n",
56
+ exit_code=1,
57
+ )
58
+ elif arg.startswith("-b"):
59
+ bytes_per_file = self._parse_size(arg[2:])
60
+ if bytes_per_file is None:
61
+ return ExecResult(
62
+ stdout="",
63
+ stderr=f"split: invalid number of bytes: '{arg[2:]}'\n",
64
+ exit_code=1,
65
+ )
66
+ elif arg == "-n" and i + 1 < len(args):
67
+ i += 1
68
+ try:
69
+ num_chunks = int(args[i])
70
+ if num_chunks <= 0:
71
+ raise ValueError("must be positive")
72
+ except ValueError:
73
+ return ExecResult(
74
+ stdout="",
75
+ stderr=f"split: invalid number of chunks: '{args[i]}'\n",
76
+ exit_code=1,
77
+ )
78
+ elif arg.startswith("-n"):
79
+ try:
80
+ num_chunks = int(arg[2:])
81
+ if num_chunks <= 0:
82
+ raise ValueError("must be positive")
83
+ except ValueError:
84
+ return ExecResult(
85
+ stdout="",
86
+ stderr=f"split: invalid number of chunks: '{arg[2:]}'\n",
87
+ exit_code=1,
88
+ )
89
+ elif arg in ("-d", "--numeric-suffixes"):
90
+ numeric_suffix = True
91
+ elif arg == "-a" and i + 1 < len(args):
92
+ i += 1
93
+ try:
94
+ suffix_length = int(args[i])
95
+ except ValueError:
96
+ return ExecResult(
97
+ stdout="",
98
+ stderr=f"split: invalid suffix length: '{args[i]}'\n",
99
+ exit_code=1,
100
+ )
101
+ elif arg.startswith("-a"):
102
+ try:
103
+ suffix_length = int(arg[2:])
104
+ except ValueError:
105
+ return ExecResult(
106
+ stdout="",
107
+ stderr=f"split: invalid suffix length: '{arg[2:]}'\n",
108
+ exit_code=1,
109
+ )
110
+ elif arg == "--":
111
+ remaining = args[i + 1:]
112
+ if remaining:
113
+ file_path = remaining[0]
114
+ if len(remaining) > 1:
115
+ prefix = remaining[1]
116
+ break
117
+ elif arg.startswith("-") and len(arg) > 1:
118
+ return ExecResult(
119
+ stdout="",
120
+ stderr=f"split: invalid option -- '{arg[1]}'\n",
121
+ exit_code=1,
122
+ )
123
+ elif file_path is None:
124
+ file_path = arg
125
+ else:
126
+ prefix = arg
127
+ i += 1
128
+
129
+ # Read content
130
+ try:
131
+ if file_path is None or file_path == "-":
132
+ content = ctx.stdin
133
+ content_bytes = content.encode("utf-8")
134
+ else:
135
+ path = ctx.fs.resolve_path(ctx.cwd, file_path)
136
+ content_bytes = await ctx.fs.read_file_bytes(path)
137
+ content = content_bytes.decode("utf-8", errors="replace")
138
+ except FileNotFoundError:
139
+ return ExecResult(
140
+ stdout="",
141
+ stderr=f"split: {file_path}: No such file or directory\n",
142
+ exit_code=1,
143
+ )
144
+
145
+ # Split the content
146
+ if num_chunks is not None:
147
+ chunks = self._split_into_chunks(content_bytes, num_chunks)
148
+ elif bytes_per_file is not None:
149
+ chunks = self._split_by_bytes(content_bytes, bytes_per_file)
150
+ else:
151
+ chunks = self._split_by_lines(content, lines_per_file)
152
+
153
+ # Write output files
154
+ for idx, chunk in enumerate(chunks):
155
+ suffix = self._generate_suffix(idx, suffix_length, numeric_suffix)
156
+ output_path = ctx.fs.resolve_path(ctx.cwd, prefix + suffix)
157
+
158
+ if isinstance(chunk, bytes):
159
+ await ctx.fs.write_file(output_path, chunk)
160
+ else:
161
+ await ctx.fs.write_file(output_path, chunk)
162
+
163
+ return ExecResult(stdout="", stderr="", exit_code=0)
164
+
165
+ def _parse_size(self, s: str) -> int | None:
166
+ """Parse a size specification like '100', '1k', '1m'."""
167
+ if not s:
168
+ return None
169
+ try:
170
+ multiplier = 1
171
+ if s[-1].lower() == "k":
172
+ multiplier = 1024
173
+ s = s[:-1]
174
+ elif s[-1].lower() == "m":
175
+ multiplier = 1024 * 1024
176
+ s = s[:-1]
177
+ elif s[-1].lower() == "g":
178
+ multiplier = 1024 * 1024 * 1024
179
+ s = s[:-1]
180
+ return int(s) * multiplier
181
+ except ValueError:
182
+ return None
183
+
184
+ def _split_by_lines(self, content: str, lines_per_file: int) -> list[str]:
185
+ """Split content by number of lines."""
186
+ lines = content.split("\n")
187
+ chunks = []
188
+
189
+ for i in range(0, len(lines), lines_per_file):
190
+ chunk_lines = lines[i:i + lines_per_file]
191
+ chunk = "\n".join(chunk_lines)
192
+ if i + lines_per_file < len(lines):
193
+ chunk += "\n"
194
+ chunks.append(chunk)
195
+
196
+ return chunks if chunks else [""]
197
+
198
+ def _split_by_bytes(self, content: bytes, bytes_per_file: int) -> list[bytes]:
199
+ """Split content by number of bytes."""
200
+ chunks = []
201
+ for i in range(0, len(content), bytes_per_file):
202
+ chunks.append(content[i:i + bytes_per_file])
203
+ return chunks if chunks else [b""]
204
+
205
+ def _split_into_chunks(self, content: bytes, num_chunks: int) -> list[bytes]:
206
+ """Split content into exactly N chunks."""
207
+ total = len(content)
208
+ if total == 0:
209
+ # Return empty chunks
210
+ return [b""] * num_chunks
211
+
212
+ # Calculate base size and remainder
213
+ base_size = total // num_chunks
214
+ remainder = total % num_chunks
215
+
216
+ chunks = []
217
+ pos = 0
218
+ for i in range(num_chunks):
219
+ # First 'remainder' chunks get one extra byte
220
+ chunk_size = base_size + (1 if i < remainder else 0)
221
+ chunks.append(content[pos:pos + chunk_size])
222
+ pos += chunk_size
223
+
224
+ return chunks
225
+
226
+ def _generate_suffix(self, idx: int, length: int, numeric: bool) -> str:
227
+ """Generate suffix for output file."""
228
+ if numeric:
229
+ return str(idx).zfill(length)
230
+ else:
231
+ # Generate alphabetic suffix (aa, ab, ..., az, ba, ...)
232
+ suffix = ""
233
+ remaining = idx
234
+ for _ in range(length):
235
+ suffix = chr(ord("a") + remaining % 26) + suffix
236
+ remaining //= 26
237
+ return suffix
@@ -0,0 +1,5 @@
1
+ """SQLite3 command - SQLite database CLI."""
2
+
3
+ from .sqlite3_cmd import Sqlite3Command
4
+
5
+ __all__ = ["Sqlite3Command"]