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,327 @@
|
|
|
1
|
+
"""Cut command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: cut OPTION... [FILE]...
|
|
4
|
+
|
|
5
|
+
Print selected parts of lines from each FILE to standard output.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-b, --bytes=LIST select only these bytes
|
|
9
|
+
-c, --characters=LIST select only these characters
|
|
10
|
+
-d, --delimiter=DELIM use DELIM instead of TAB for field delimiter
|
|
11
|
+
-f, --fields=LIST select only these fields
|
|
12
|
+
-s, --only-delimited do not print lines not containing delimiters
|
|
13
|
+
--output-delimiter=STRING use STRING as the output delimiter
|
|
14
|
+
|
|
15
|
+
LIST is made up of one range, or many ranges separated by commas.
|
|
16
|
+
Each range is one of:
|
|
17
|
+
N N'th byte, character or field, counted from 1
|
|
18
|
+
N- from N'th byte, character or field, to end of line
|
|
19
|
+
N-M from N'th to M'th byte, character or field
|
|
20
|
+
-M from first to M'th byte, character or field
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from ...types import CommandContext, ExecResult
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CutCommand:
|
|
27
|
+
"""The cut command."""
|
|
28
|
+
|
|
29
|
+
name = "cut"
|
|
30
|
+
|
|
31
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
32
|
+
"""Execute the cut command."""
|
|
33
|
+
byte_list = ""
|
|
34
|
+
char_list = ""
|
|
35
|
+
field_list = ""
|
|
36
|
+
delimiter = "\t"
|
|
37
|
+
output_delimiter = None
|
|
38
|
+
only_delimited = False
|
|
39
|
+
files: list[str] = []
|
|
40
|
+
|
|
41
|
+
# Parse arguments
|
|
42
|
+
i = 0
|
|
43
|
+
while i < len(args):
|
|
44
|
+
arg = args[i]
|
|
45
|
+
if arg == "--":
|
|
46
|
+
files.extend(args[i + 1:])
|
|
47
|
+
break
|
|
48
|
+
elif arg.startswith("--"):
|
|
49
|
+
if arg.startswith("--bytes="):
|
|
50
|
+
byte_list = arg[8:]
|
|
51
|
+
elif arg.startswith("--characters="):
|
|
52
|
+
char_list = arg[13:]
|
|
53
|
+
elif arg.startswith("--delimiter="):
|
|
54
|
+
delimiter = arg[12:]
|
|
55
|
+
if len(delimiter) != 1:
|
|
56
|
+
return ExecResult(
|
|
57
|
+
stdout="",
|
|
58
|
+
stderr="cut: the delimiter must be a single character\n",
|
|
59
|
+
exit_code=1,
|
|
60
|
+
)
|
|
61
|
+
elif arg.startswith("--fields="):
|
|
62
|
+
field_list = arg[9:]
|
|
63
|
+
elif arg == "--only-delimited":
|
|
64
|
+
only_delimited = True
|
|
65
|
+
elif arg.startswith("--output-delimiter="):
|
|
66
|
+
output_delimiter = arg[19:]
|
|
67
|
+
else:
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="",
|
|
70
|
+
stderr=f"cut: unrecognized option '{arg}'\n",
|
|
71
|
+
exit_code=1,
|
|
72
|
+
)
|
|
73
|
+
elif arg.startswith("-") and arg != "-":
|
|
74
|
+
j = 1
|
|
75
|
+
while j < len(arg):
|
|
76
|
+
c = arg[j]
|
|
77
|
+
if c == "b":
|
|
78
|
+
# -b requires a value
|
|
79
|
+
if j + 1 < len(arg):
|
|
80
|
+
byte_list = arg[j + 1:]
|
|
81
|
+
break
|
|
82
|
+
elif i + 1 < len(args):
|
|
83
|
+
i += 1
|
|
84
|
+
byte_list = args[i]
|
|
85
|
+
break
|
|
86
|
+
else:
|
|
87
|
+
return ExecResult(
|
|
88
|
+
stdout="",
|
|
89
|
+
stderr="cut: option requires an argument -- 'b'\n",
|
|
90
|
+
exit_code=1,
|
|
91
|
+
)
|
|
92
|
+
elif c == "c":
|
|
93
|
+
# -c requires a value
|
|
94
|
+
if j + 1 < len(arg):
|
|
95
|
+
char_list = arg[j + 1:]
|
|
96
|
+
break
|
|
97
|
+
elif i + 1 < len(args):
|
|
98
|
+
i += 1
|
|
99
|
+
char_list = args[i]
|
|
100
|
+
break
|
|
101
|
+
else:
|
|
102
|
+
return ExecResult(
|
|
103
|
+
stdout="",
|
|
104
|
+
stderr="cut: option requires an argument -- 'c'\n",
|
|
105
|
+
exit_code=1,
|
|
106
|
+
)
|
|
107
|
+
elif c == "d":
|
|
108
|
+
# -d requires a value
|
|
109
|
+
if j + 1 < len(arg):
|
|
110
|
+
delimiter = arg[j + 1:]
|
|
111
|
+
break
|
|
112
|
+
elif i + 1 < len(args):
|
|
113
|
+
i += 1
|
|
114
|
+
delimiter = args[i]
|
|
115
|
+
break
|
|
116
|
+
else:
|
|
117
|
+
return ExecResult(
|
|
118
|
+
stdout="",
|
|
119
|
+
stderr="cut: option requires an argument -- 'd'\n",
|
|
120
|
+
exit_code=1,
|
|
121
|
+
)
|
|
122
|
+
if len(delimiter) != 1:
|
|
123
|
+
return ExecResult(
|
|
124
|
+
stdout="",
|
|
125
|
+
stderr="cut: the delimiter must be a single character\n",
|
|
126
|
+
exit_code=1,
|
|
127
|
+
)
|
|
128
|
+
elif c == "f":
|
|
129
|
+
# -f requires a value
|
|
130
|
+
if j + 1 < len(arg):
|
|
131
|
+
field_list = arg[j + 1:]
|
|
132
|
+
break
|
|
133
|
+
elif i + 1 < len(args):
|
|
134
|
+
i += 1
|
|
135
|
+
field_list = args[i]
|
|
136
|
+
break
|
|
137
|
+
else:
|
|
138
|
+
return ExecResult(
|
|
139
|
+
stdout="",
|
|
140
|
+
stderr="cut: option requires an argument -- 'f'\n",
|
|
141
|
+
exit_code=1,
|
|
142
|
+
)
|
|
143
|
+
elif c == "s":
|
|
144
|
+
only_delimited = True
|
|
145
|
+
else:
|
|
146
|
+
return ExecResult(
|
|
147
|
+
stdout="",
|
|
148
|
+
stderr=f"cut: invalid option -- '{c}'\n",
|
|
149
|
+
exit_code=1,
|
|
150
|
+
)
|
|
151
|
+
j += 1
|
|
152
|
+
else:
|
|
153
|
+
files.append(arg)
|
|
154
|
+
i += 1
|
|
155
|
+
|
|
156
|
+
# Validate - need exactly one of -b, -c, or -f
|
|
157
|
+
modes = sum([bool(byte_list), bool(char_list), bool(field_list)])
|
|
158
|
+
if modes == 0:
|
|
159
|
+
return ExecResult(
|
|
160
|
+
stdout="",
|
|
161
|
+
stderr="cut: you must specify a list of bytes, characters, or fields\n",
|
|
162
|
+
exit_code=1,
|
|
163
|
+
)
|
|
164
|
+
if modes > 1:
|
|
165
|
+
return ExecResult(
|
|
166
|
+
stdout="",
|
|
167
|
+
stderr="cut: only one type of list may be specified\n",
|
|
168
|
+
exit_code=1,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Parse the list
|
|
172
|
+
try:
|
|
173
|
+
if byte_list:
|
|
174
|
+
ranges = self._parse_list(byte_list)
|
|
175
|
+
mode = "bytes"
|
|
176
|
+
elif char_list:
|
|
177
|
+
ranges = self._parse_list(char_list)
|
|
178
|
+
mode = "chars"
|
|
179
|
+
else:
|
|
180
|
+
ranges = self._parse_list(field_list)
|
|
181
|
+
mode = "fields"
|
|
182
|
+
except ValueError as e:
|
|
183
|
+
return ExecResult(
|
|
184
|
+
stdout="",
|
|
185
|
+
stderr=f"cut: {e}\n",
|
|
186
|
+
exit_code=1,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Set output delimiter
|
|
190
|
+
if output_delimiter is None:
|
|
191
|
+
output_delimiter = delimiter
|
|
192
|
+
|
|
193
|
+
# Default to stdin
|
|
194
|
+
if not files:
|
|
195
|
+
files = ["-"]
|
|
196
|
+
|
|
197
|
+
stdout = ""
|
|
198
|
+
stderr = ""
|
|
199
|
+
exit_code = 0
|
|
200
|
+
|
|
201
|
+
for f in files:
|
|
202
|
+
try:
|
|
203
|
+
if f == "-":
|
|
204
|
+
content = ctx.stdin
|
|
205
|
+
else:
|
|
206
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
207
|
+
content = await ctx.fs.read_file(path)
|
|
208
|
+
|
|
209
|
+
lines = content.split("\n")
|
|
210
|
+
# Remove trailing empty line if present
|
|
211
|
+
if lines and lines[-1] == "":
|
|
212
|
+
lines = lines[:-1]
|
|
213
|
+
|
|
214
|
+
for line in lines:
|
|
215
|
+
if mode == "fields":
|
|
216
|
+
result = self._cut_fields(line, ranges, delimiter, output_delimiter, only_delimited)
|
|
217
|
+
else:
|
|
218
|
+
result = self._cut_chars(line, ranges)
|
|
219
|
+
|
|
220
|
+
if result is not None:
|
|
221
|
+
stdout += result + "\n"
|
|
222
|
+
|
|
223
|
+
except FileNotFoundError:
|
|
224
|
+
stderr += f"cut: {f}: No such file or directory\n"
|
|
225
|
+
exit_code = 1
|
|
226
|
+
|
|
227
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
228
|
+
|
|
229
|
+
def _parse_list(self, list_str: str) -> list[tuple[int, int | None]]:
|
|
230
|
+
"""Parse a list specification into ranges.
|
|
231
|
+
|
|
232
|
+
Returns list of (start, end) tuples. end=None means to end of line.
|
|
233
|
+
Indices are 0-based internally.
|
|
234
|
+
"""
|
|
235
|
+
ranges: list[tuple[int, int | None]] = []
|
|
236
|
+
|
|
237
|
+
for part in list_str.split(","):
|
|
238
|
+
part = part.strip()
|
|
239
|
+
if not part:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if "-" in part:
|
|
243
|
+
if part.startswith("-"):
|
|
244
|
+
# -M: from start to M
|
|
245
|
+
try:
|
|
246
|
+
end = int(part[1:])
|
|
247
|
+
ranges.append((0, end))
|
|
248
|
+
except ValueError:
|
|
249
|
+
raise ValueError(f"invalid byte, character or field list: {list_str}")
|
|
250
|
+
elif part.endswith("-"):
|
|
251
|
+
# N-: from N to end
|
|
252
|
+
try:
|
|
253
|
+
start = int(part[:-1])
|
|
254
|
+
if start < 1:
|
|
255
|
+
raise ValueError(f"fields and positions are numbered from 1")
|
|
256
|
+
ranges.append((start - 1, None))
|
|
257
|
+
except ValueError:
|
|
258
|
+
raise ValueError(f"invalid byte, character or field list: {list_str}")
|
|
259
|
+
else:
|
|
260
|
+
# N-M
|
|
261
|
+
parts = part.split("-", 1)
|
|
262
|
+
try:
|
|
263
|
+
start = int(parts[0])
|
|
264
|
+
end = int(parts[1])
|
|
265
|
+
if start < 1:
|
|
266
|
+
raise ValueError(f"fields and positions are numbered from 1")
|
|
267
|
+
ranges.append((start - 1, end))
|
|
268
|
+
except ValueError:
|
|
269
|
+
raise ValueError(f"invalid byte, character or field list: {list_str}")
|
|
270
|
+
else:
|
|
271
|
+
# Single number
|
|
272
|
+
try:
|
|
273
|
+
n = int(part)
|
|
274
|
+
if n < 1:
|
|
275
|
+
raise ValueError(f"fields and positions are numbered from 1")
|
|
276
|
+
ranges.append((n - 1, n))
|
|
277
|
+
except ValueError:
|
|
278
|
+
raise ValueError(f"invalid byte, character or field list: {list_str}")
|
|
279
|
+
|
|
280
|
+
return ranges
|
|
281
|
+
|
|
282
|
+
def _cut_chars(self, line: str, ranges: list[tuple[int, int | None]]) -> str:
|
|
283
|
+
"""Cut characters/bytes from a line."""
|
|
284
|
+
result_chars: list[str] = []
|
|
285
|
+
included = set()
|
|
286
|
+
|
|
287
|
+
for start, end in ranges:
|
|
288
|
+
if end is None:
|
|
289
|
+
end = len(line)
|
|
290
|
+
for i in range(start, min(end, len(line))):
|
|
291
|
+
if i not in included:
|
|
292
|
+
included.add(i)
|
|
293
|
+
result_chars.append((i, line[i]))
|
|
294
|
+
|
|
295
|
+
# Sort by position and extract chars
|
|
296
|
+
result_chars.sort(key=lambda x: x[0])
|
|
297
|
+
return "".join(c for _, c in result_chars)
|
|
298
|
+
|
|
299
|
+
def _cut_fields(
|
|
300
|
+
self,
|
|
301
|
+
line: str,
|
|
302
|
+
ranges: list[tuple[int, int | None]],
|
|
303
|
+
delimiter: str,
|
|
304
|
+
output_delimiter: str,
|
|
305
|
+
only_delimited: bool,
|
|
306
|
+
) -> str | None:
|
|
307
|
+
"""Cut fields from a line."""
|
|
308
|
+
if delimiter not in line:
|
|
309
|
+
if only_delimited:
|
|
310
|
+
return None
|
|
311
|
+
return line
|
|
312
|
+
|
|
313
|
+
fields = line.split(delimiter)
|
|
314
|
+
result_fields: list[tuple[int, str]] = []
|
|
315
|
+
included = set()
|
|
316
|
+
|
|
317
|
+
for start, end in ranges:
|
|
318
|
+
if end is None:
|
|
319
|
+
end = len(fields)
|
|
320
|
+
for i in range(start, min(end, len(fields))):
|
|
321
|
+
if i not in included:
|
|
322
|
+
included.add(i)
|
|
323
|
+
result_fields.append((i, fields[i]))
|
|
324
|
+
|
|
325
|
+
# Sort by position and extract fields
|
|
326
|
+
result_fields.sort(key=lambda x: x[0])
|
|
327
|
+
return output_delimiter.join(f for _, f in result_fields)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Date command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: date [OPTION]... [+FORMAT]
|
|
4
|
+
|
|
5
|
+
Display the current time in the given FORMAT.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-d, --date=STRING display time described by STRING
|
|
9
|
+
-u, --utc print Coordinated Universal Time (UTC)
|
|
10
|
+
-I, --iso-8601 output date/time in ISO 8601 format
|
|
11
|
+
-R, --rfc-email output date and time in RFC 5322 format
|
|
12
|
+
|
|
13
|
+
FORMAT controls the output. Common sequences:
|
|
14
|
+
%a abbreviated weekday name (Sun..Sat)
|
|
15
|
+
%A full weekday name (Sunday..Saturday)
|
|
16
|
+
%b abbreviated month name (Jan..Dec)
|
|
17
|
+
%B full month name (January..December)
|
|
18
|
+
%d day of month (01..31)
|
|
19
|
+
%H hour (00..23)
|
|
20
|
+
%I hour (01..12)
|
|
21
|
+
%j day of year (001..366)
|
|
22
|
+
%m month (01..12)
|
|
23
|
+
%M minute (00..59)
|
|
24
|
+
%p AM or PM
|
|
25
|
+
%S second (00..60)
|
|
26
|
+
%Y year
|
|
27
|
+
%Z timezone name
|
|
28
|
+
%z +hhmm numeric timezone
|
|
29
|
+
%F full date; same as %Y-%m-%d
|
|
30
|
+
%T time; same as %H:%M:%S
|
|
31
|
+
%s seconds since 1970-01-01 00:00:00 UTC
|
|
32
|
+
%% a literal %
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from ...types import CommandContext, ExecResult
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DateCommand:
|
|
40
|
+
"""The date command."""
|
|
41
|
+
|
|
42
|
+
name = "date"
|
|
43
|
+
|
|
44
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
45
|
+
"""Execute the date command."""
|
|
46
|
+
date_string = None
|
|
47
|
+
use_utc = False
|
|
48
|
+
iso_format = False
|
|
49
|
+
rfc_format = False
|
|
50
|
+
format_str = None
|
|
51
|
+
|
|
52
|
+
# Parse arguments
|
|
53
|
+
i = 0
|
|
54
|
+
while i < len(args):
|
|
55
|
+
arg = args[i]
|
|
56
|
+
if arg.startswith("--"):
|
|
57
|
+
if arg == "--utc" or arg == "--universal":
|
|
58
|
+
use_utc = True
|
|
59
|
+
elif arg.startswith("--date="):
|
|
60
|
+
date_string = arg[7:]
|
|
61
|
+
elif arg == "--iso-8601":
|
|
62
|
+
iso_format = True
|
|
63
|
+
elif arg == "--rfc-email" or arg == "--rfc-2822":
|
|
64
|
+
rfc_format = True
|
|
65
|
+
else:
|
|
66
|
+
return ExecResult(
|
|
67
|
+
stdout="",
|
|
68
|
+
stderr=f"date: unrecognized option '{arg}'\n",
|
|
69
|
+
exit_code=1,
|
|
70
|
+
)
|
|
71
|
+
elif arg.startswith("-") and arg != "-":
|
|
72
|
+
j = 1
|
|
73
|
+
while j < len(arg):
|
|
74
|
+
c = arg[j]
|
|
75
|
+
if c == "u":
|
|
76
|
+
use_utc = True
|
|
77
|
+
elif c == "d":
|
|
78
|
+
# -d requires a value
|
|
79
|
+
if j + 1 < len(arg):
|
|
80
|
+
date_string = arg[j + 1:]
|
|
81
|
+
break
|
|
82
|
+
elif i + 1 < len(args):
|
|
83
|
+
i += 1
|
|
84
|
+
date_string = args[i]
|
|
85
|
+
break
|
|
86
|
+
else:
|
|
87
|
+
return ExecResult(
|
|
88
|
+
stdout="",
|
|
89
|
+
stderr="date: option requires an argument -- 'd'\n",
|
|
90
|
+
exit_code=1,
|
|
91
|
+
)
|
|
92
|
+
elif c == "I":
|
|
93
|
+
iso_format = True
|
|
94
|
+
elif c == "R":
|
|
95
|
+
rfc_format = True
|
|
96
|
+
else:
|
|
97
|
+
return ExecResult(
|
|
98
|
+
stdout="",
|
|
99
|
+
stderr=f"date: invalid option -- '{c}'\n",
|
|
100
|
+
exit_code=1,
|
|
101
|
+
)
|
|
102
|
+
j += 1
|
|
103
|
+
elif arg.startswith("+"):
|
|
104
|
+
format_str = arg[1:]
|
|
105
|
+
else:
|
|
106
|
+
return ExecResult(
|
|
107
|
+
stdout="",
|
|
108
|
+
stderr=f"date: invalid date '{arg}'\n",
|
|
109
|
+
exit_code=1,
|
|
110
|
+
)
|
|
111
|
+
i += 1
|
|
112
|
+
|
|
113
|
+
# Get the datetime
|
|
114
|
+
try:
|
|
115
|
+
if date_string:
|
|
116
|
+
dt = self._parse_date_string(date_string)
|
|
117
|
+
else:
|
|
118
|
+
dt = datetime.now()
|
|
119
|
+
|
|
120
|
+
if use_utc:
|
|
121
|
+
dt = dt.astimezone(timezone.utc)
|
|
122
|
+
except ValueError as e:
|
|
123
|
+
return ExecResult(
|
|
124
|
+
stdout="",
|
|
125
|
+
stderr=f"date: invalid date '{date_string}'\n",
|
|
126
|
+
exit_code=1,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Format output
|
|
130
|
+
if iso_format:
|
|
131
|
+
output = dt.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
132
|
+
if not output.endswith("Z") and len(output) > 5 and output[-5] in "+-":
|
|
133
|
+
# Insert colon in timezone offset
|
|
134
|
+
output = output[:-2] + ":" + output[-2:]
|
|
135
|
+
elif rfc_format:
|
|
136
|
+
output = dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
137
|
+
elif format_str:
|
|
138
|
+
output = self._format_date(dt, format_str)
|
|
139
|
+
else:
|
|
140
|
+
# Default format
|
|
141
|
+
output = dt.strftime("%a %b %d %H:%M:%S %Z %Y")
|
|
142
|
+
if not output.strip():
|
|
143
|
+
output = dt.strftime("%a %b %d %H:%M:%S UTC %Y")
|
|
144
|
+
|
|
145
|
+
return ExecResult(stdout=output + "\n", stderr="", exit_code=0)
|
|
146
|
+
|
|
147
|
+
def _parse_date_string(self, s: str) -> datetime:
|
|
148
|
+
"""Parse a date string."""
|
|
149
|
+
s = s.strip()
|
|
150
|
+
|
|
151
|
+
# Handle special keywords
|
|
152
|
+
if s.lower() == "now":
|
|
153
|
+
return datetime.now()
|
|
154
|
+
if s.lower() == "today":
|
|
155
|
+
return datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
156
|
+
if s.lower() == "yesterday":
|
|
157
|
+
from datetime import timedelta
|
|
158
|
+
return (datetime.now() - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
159
|
+
if s.lower() == "tomorrow":
|
|
160
|
+
from datetime import timedelta
|
|
161
|
+
return (datetime.now() + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
162
|
+
|
|
163
|
+
# Try ISO format
|
|
164
|
+
for fmt in [
|
|
165
|
+
"%Y-%m-%dT%H:%M:%S%z",
|
|
166
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
167
|
+
"%Y-%m-%d %H:%M:%S",
|
|
168
|
+
"%Y-%m-%d",
|
|
169
|
+
"%m/%d/%Y",
|
|
170
|
+
"%d %b %Y",
|
|
171
|
+
"%b %d %Y",
|
|
172
|
+
]:
|
|
173
|
+
try:
|
|
174
|
+
return datetime.strptime(s, fmt)
|
|
175
|
+
except ValueError:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Try parsing as Unix timestamp
|
|
179
|
+
try:
|
|
180
|
+
ts = float(s)
|
|
181
|
+
return datetime.fromtimestamp(ts)
|
|
182
|
+
except ValueError:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
raise ValueError(f"Unable to parse date: {s}")
|
|
186
|
+
|
|
187
|
+
def _format_date(self, dt: datetime, fmt: str) -> str:
|
|
188
|
+
"""Format a date using strftime-like codes."""
|
|
189
|
+
result = ""
|
|
190
|
+
i = 0
|
|
191
|
+
while i < len(fmt):
|
|
192
|
+
if fmt[i] == "%" and i + 1 < len(fmt):
|
|
193
|
+
code = fmt[i + 1]
|
|
194
|
+
if code == "%":
|
|
195
|
+
result += "%"
|
|
196
|
+
elif code == "a":
|
|
197
|
+
result += dt.strftime("%a")
|
|
198
|
+
elif code == "A":
|
|
199
|
+
result += dt.strftime("%A")
|
|
200
|
+
elif code == "b":
|
|
201
|
+
result += dt.strftime("%b")
|
|
202
|
+
elif code == "B":
|
|
203
|
+
result += dt.strftime("%B")
|
|
204
|
+
elif code == "d":
|
|
205
|
+
result += dt.strftime("%d")
|
|
206
|
+
elif code == "D":
|
|
207
|
+
result += dt.strftime("%m/%d/%y")
|
|
208
|
+
elif code == "e":
|
|
209
|
+
result += f"{dt.day:2d}"
|
|
210
|
+
elif code == "F":
|
|
211
|
+
result += dt.strftime("%Y-%m-%d")
|
|
212
|
+
elif code == "H":
|
|
213
|
+
result += dt.strftime("%H")
|
|
214
|
+
elif code == "I":
|
|
215
|
+
result += dt.strftime("%I")
|
|
216
|
+
elif code == "j":
|
|
217
|
+
result += dt.strftime("%j")
|
|
218
|
+
elif code == "m":
|
|
219
|
+
result += dt.strftime("%m")
|
|
220
|
+
elif code == "M":
|
|
221
|
+
result += dt.strftime("%M")
|
|
222
|
+
elif code == "n":
|
|
223
|
+
result += "\n"
|
|
224
|
+
elif code == "p":
|
|
225
|
+
result += dt.strftime("%p")
|
|
226
|
+
elif code == "P":
|
|
227
|
+
result += dt.strftime("%p").lower()
|
|
228
|
+
elif code == "S":
|
|
229
|
+
result += dt.strftime("%S")
|
|
230
|
+
elif code == "s":
|
|
231
|
+
result += str(int(dt.timestamp()))
|
|
232
|
+
elif code == "t":
|
|
233
|
+
result += "\t"
|
|
234
|
+
elif code == "T":
|
|
235
|
+
result += dt.strftime("%H:%M:%S")
|
|
236
|
+
elif code == "u":
|
|
237
|
+
# Day of week (1=Monday, 7=Sunday)
|
|
238
|
+
result += str(dt.isoweekday())
|
|
239
|
+
elif code == "w":
|
|
240
|
+
# Day of week (0=Sunday, 6=Saturday)
|
|
241
|
+
result += str((dt.weekday() + 1) % 7)
|
|
242
|
+
elif code == "W":
|
|
243
|
+
result += dt.strftime("%W")
|
|
244
|
+
elif code == "Y":
|
|
245
|
+
result += dt.strftime("%Y")
|
|
246
|
+
elif code == "y":
|
|
247
|
+
result += dt.strftime("%y")
|
|
248
|
+
elif code == "z":
|
|
249
|
+
result += dt.strftime("%z") or "+0000"
|
|
250
|
+
elif code == "Z":
|
|
251
|
+
result += dt.strftime("%Z") or "UTC"
|
|
252
|
+
else:
|
|
253
|
+
result += "%" + code
|
|
254
|
+
i += 2
|
|
255
|
+
else:
|
|
256
|
+
result += fmt[i]
|
|
257
|
+
i += 1
|
|
258
|
+
return result
|