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,5 @@
1
+ """Cut command implementation."""
2
+
3
+ from .cut import CutCommand
4
+
5
+ __all__ = ["CutCommand"]
@@ -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,5 @@
1
+ """Date command implementation."""
2
+
3
+ from .date import DateCommand
4
+
5
+ __all__ = ["DateCommand"]
@@ -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
@@ -0,0 +1,5 @@
1
+ """Diff command."""
2
+
3
+ from .diff import DiffCommand
4
+
5
+ __all__ = ["DiffCommand"]