just-bash 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,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,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
|