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,505 @@
1
+ """SQLite3 command implementation (in-memory only).
2
+
3
+ Usage: sqlite3 [OPTIONS] :memory: [SQL]
4
+
5
+ SQLite database CLI. Only in-memory databases (:memory:) are supported.
6
+
7
+ Options:
8
+ -list output in list mode (default)
9
+ -csv output in CSV mode
10
+ -json output in JSON mode
11
+ -line output in line mode
12
+ -column output in column mode
13
+ -header show column headers
14
+ -noheader hide column headers
15
+ -separator SEP field separator (default: |)
16
+ -nullvalue TEXT text for NULL values
17
+ -bail stop on first error
18
+ -echo print SQL before execution
19
+ -version show SQLite version
20
+ """
21
+
22
+ import json
23
+ import sqlite3
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from ...types import CommandContext, ExecResult
28
+
29
+
30
+ @dataclass
31
+ class Sqlite3Options:
32
+ """Parsed sqlite3 options."""
33
+ mode: str = "list"
34
+ header: bool = False
35
+ separator: str = "|"
36
+ newline: str = "\n"
37
+ null_value: str = ""
38
+ readonly: bool = False
39
+ bail: bool = False
40
+ echo: bool = False
41
+ cmd: str | None = None
42
+
43
+
44
+ def format_value(val: Any, null_value: str = "") -> str:
45
+ """Format a SQL value for output."""
46
+ if val is None:
47
+ return null_value
48
+ return str(val)
49
+
50
+
51
+ def format_output(
52
+ columns: list[str],
53
+ rows: list[tuple[Any, ...]],
54
+ options: Sqlite3Options,
55
+ ) -> str:
56
+ """Format query results based on output mode."""
57
+ if options.mode == "list":
58
+ return format_list(columns, rows, options)
59
+ elif options.mode == "csv":
60
+ return format_csv(columns, rows, options)
61
+ elif options.mode == "json":
62
+ return format_json(columns, rows, options)
63
+ elif options.mode == "line":
64
+ return format_line(columns, rows, options)
65
+ elif options.mode == "column":
66
+ return format_column(columns, rows, options)
67
+ elif options.mode == "table":
68
+ return format_table(columns, rows, options)
69
+ elif options.mode == "tabs":
70
+ return format_tabs(columns, rows, options)
71
+ elif options.mode == "markdown":
72
+ return format_markdown(columns, rows, options)
73
+ else:
74
+ return format_list(columns, rows, options)
75
+
76
+
77
+ def format_list(
78
+ columns: list[str],
79
+ rows: list[tuple[Any, ...]],
80
+ options: Sqlite3Options,
81
+ ) -> str:
82
+ """Format as pipe-separated list."""
83
+ lines = []
84
+ if options.header and columns:
85
+ lines.append(options.separator.join(columns))
86
+ for row in rows:
87
+ lines.append(options.separator.join(format_value(v, options.null_value) for v in row))
88
+ return options.newline.join(lines) + (options.newline if lines else "")
89
+
90
+
91
+ def format_csv(
92
+ columns: list[str],
93
+ rows: list[tuple[Any, ...]],
94
+ options: Sqlite3Options,
95
+ ) -> str:
96
+ """Format as CSV."""
97
+ import csv
98
+ import io
99
+
100
+ output = io.StringIO(newline="")
101
+ writer = csv.writer(output, lineterminator="\n")
102
+ if options.header and columns:
103
+ writer.writerow(columns)
104
+ for row in rows:
105
+ writer.writerow([format_value(v, options.null_value) for v in row])
106
+ return output.getvalue()
107
+
108
+
109
+ def format_json(
110
+ columns: list[str],
111
+ rows: list[tuple[Any, ...]],
112
+ options: Sqlite3Options,
113
+ ) -> str:
114
+ """Format as JSON array of objects."""
115
+ result = []
116
+ for row in rows:
117
+ obj = {}
118
+ for i, col in enumerate(columns):
119
+ val = row[i] if i < len(row) else None
120
+ obj[col] = val
121
+ result.append(obj)
122
+ return json.dumps(result) + "\n"
123
+
124
+
125
+ def format_line(
126
+ columns: list[str],
127
+ rows: list[tuple[Any, ...]],
128
+ options: Sqlite3Options,
129
+ ) -> str:
130
+ """Format as column = value, one per line."""
131
+ lines = []
132
+ for row in rows:
133
+ for i, col in enumerate(columns):
134
+ val = row[i] if i < len(row) else None
135
+ lines.append(f"{col} = {format_value(val, options.null_value)}")
136
+ lines.append("")
137
+ return "\n".join(lines)
138
+
139
+
140
+ def format_column(
141
+ columns: list[str],
142
+ rows: list[tuple[Any, ...]],
143
+ options: Sqlite3Options,
144
+ ) -> str:
145
+ """Format as fixed-width columns."""
146
+ if not columns:
147
+ return ""
148
+
149
+ # Calculate column widths
150
+ widths = [len(c) for c in columns]
151
+ for row in rows:
152
+ for i, val in enumerate(row):
153
+ if i < len(widths):
154
+ widths[i] = max(widths[i], len(format_value(val, options.null_value)))
155
+
156
+ lines = []
157
+ if options.header:
158
+ lines.append(" ".join(c.ljust(widths[i]) for i, c in enumerate(columns)))
159
+ lines.append(" ".join("-" * w for w in widths))
160
+
161
+ for row in rows:
162
+ line_parts = []
163
+ for i, val in enumerate(row):
164
+ if i < len(widths):
165
+ line_parts.append(format_value(val, options.null_value).ljust(widths[i]))
166
+ lines.append(" ".join(line_parts))
167
+
168
+ return "\n".join(lines) + "\n" if lines else ""
169
+
170
+
171
+ def format_table(
172
+ columns: list[str],
173
+ rows: list[tuple[Any, ...]],
174
+ options: Sqlite3Options,
175
+ ) -> str:
176
+ """Format as ASCII table."""
177
+ if not columns:
178
+ return ""
179
+
180
+ # Calculate column widths
181
+ widths = [len(c) for c in columns]
182
+ for row in rows:
183
+ for i, val in enumerate(row):
184
+ if i < len(widths):
185
+ widths[i] = max(widths[i], len(format_value(val, options.null_value)))
186
+
187
+ # Build table
188
+ sep = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
189
+ lines = [sep]
190
+
191
+ if options.header:
192
+ header_line = "|" + "|".join(f" {c.ljust(widths[i])} " for i, c in enumerate(columns)) + "|"
193
+ lines.append(header_line)
194
+ lines.append(sep)
195
+
196
+ for row in rows:
197
+ row_parts = []
198
+ for i, val in enumerate(row):
199
+ if i < len(widths):
200
+ row_parts.append(f" {format_value(val, options.null_value).ljust(widths[i])} ")
201
+ lines.append("|" + "|".join(row_parts) + "|")
202
+
203
+ lines.append(sep)
204
+ return "\n".join(lines) + "\n"
205
+
206
+
207
+ def format_tabs(
208
+ columns: list[str],
209
+ rows: list[tuple[Any, ...]],
210
+ options: Sqlite3Options,
211
+ ) -> str:
212
+ """Format as tab-separated values."""
213
+ lines = []
214
+ if options.header and columns:
215
+ lines.append("\t".join(columns))
216
+ for row in rows:
217
+ lines.append("\t".join(format_value(v, options.null_value) for v in row))
218
+ return "\n".join(lines) + ("\n" if lines else "")
219
+
220
+
221
+ def format_markdown(
222
+ columns: list[str],
223
+ rows: list[tuple[Any, ...]],
224
+ options: Sqlite3Options,
225
+ ) -> str:
226
+ """Format as markdown table."""
227
+ if not columns:
228
+ return ""
229
+
230
+ # Calculate column widths
231
+ widths = [len(c) for c in columns]
232
+ for row in rows:
233
+ for i, val in enumerate(row):
234
+ if i < len(widths):
235
+ widths[i] = max(widths[i], len(format_value(val, options.null_value)))
236
+
237
+ lines = []
238
+
239
+ # Header
240
+ header_line = "|" + "|".join(f" {c.ljust(widths[i])} " for i, c in enumerate(columns)) + "|"
241
+ lines.append(header_line)
242
+
243
+ # Separator
244
+ sep_line = "|" + "|".join("-" * (w + 2) for w in widths) + "|"
245
+ lines.append(sep_line)
246
+
247
+ # Rows
248
+ for row in rows:
249
+ row_parts = []
250
+ for i, val in enumerate(row):
251
+ if i < len(widths):
252
+ row_parts.append(f" {format_value(val, options.null_value).ljust(widths[i])} ")
253
+ lines.append("|" + "|".join(row_parts) + "|")
254
+
255
+ return "\n".join(lines) + "\n"
256
+
257
+
258
+ def split_statements(sql: str) -> list[str]:
259
+ """Split SQL into individual statements."""
260
+ statements = []
261
+ current = ""
262
+ in_string = False
263
+ string_char = ""
264
+
265
+ for i, char in enumerate(sql):
266
+ if in_string:
267
+ current += char
268
+ if char == string_char:
269
+ if i + 1 < len(sql) and sql[i + 1] == string_char:
270
+ current += sql[i + 1]
271
+ else:
272
+ in_string = False
273
+ elif char in ("'", '"'):
274
+ current += char
275
+ in_string = True
276
+ string_char = char
277
+ elif char == ";":
278
+ stmt = current.strip()
279
+ if stmt:
280
+ statements.append(stmt)
281
+ current = ""
282
+ else:
283
+ current += char
284
+
285
+ stmt = current.strip()
286
+ if stmt:
287
+ statements.append(stmt)
288
+
289
+ return statements
290
+
291
+
292
+ def is_write_statement(sql: str) -> bool:
293
+ """Check if SQL statement modifies database."""
294
+ trimmed = sql.strip().upper()
295
+ return any(trimmed.startswith(kw) for kw in [
296
+ "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "REPLACE", "VACUUM"
297
+ ])
298
+
299
+
300
+ class Sqlite3Command:
301
+ """The sqlite3 command - SQLite database CLI."""
302
+
303
+ name = "sqlite3"
304
+
305
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
306
+ """Execute the sqlite3 command."""
307
+ if "--help" in args or "-help" in args or "-h" in args:
308
+ return ExecResult(
309
+ stdout=(
310
+ "Usage: sqlite3 [OPTIONS] DATABASE [SQL]\n"
311
+ "SQLite database CLI.\n\n"
312
+ "Options:\n"
313
+ " -list output in list mode (default)\n"
314
+ " -csv output in CSV mode\n"
315
+ " -json output in JSON mode\n"
316
+ " -line output in line mode\n"
317
+ " -column output in column mode\n"
318
+ " -table output as ASCII table\n"
319
+ " -markdown output as markdown table\n"
320
+ " -tabs output in tab-separated mode\n"
321
+ " -header show column headers\n"
322
+ " -noheader hide column headers\n"
323
+ " -separator SEP field separator (default: |)\n"
324
+ " -nullvalue TEXT text for NULL values\n"
325
+ " -readonly open database read-only\n"
326
+ " -bail stop on first error\n"
327
+ " -echo print SQL before execution\n"
328
+ " -version show SQLite version\n"
329
+ " --help show this help\n"
330
+ ),
331
+ stderr="",
332
+ exit_code=0,
333
+ )
334
+
335
+ # Parse arguments
336
+ options = Sqlite3Options()
337
+ database = None
338
+ sql_arg = None
339
+ show_version = False
340
+ end_of_options = False
341
+
342
+ i = 0
343
+ while i < len(args):
344
+ arg = args[i]
345
+
346
+ if end_of_options:
347
+ if database is None:
348
+ database = arg
349
+ elif sql_arg is None:
350
+ sql_arg = arg
351
+ i += 1
352
+ continue
353
+
354
+ if arg == "--":
355
+ end_of_options = True
356
+ elif arg == "-version":
357
+ show_version = True
358
+ elif arg == "-list":
359
+ options.mode = "list"
360
+ elif arg == "-csv":
361
+ options.mode = "csv"
362
+ elif arg == "-json":
363
+ options.mode = "json"
364
+ elif arg == "-line":
365
+ options.mode = "line"
366
+ elif arg == "-column":
367
+ options.mode = "column"
368
+ elif arg == "-table":
369
+ options.mode = "table"
370
+ elif arg == "-markdown":
371
+ options.mode = "markdown"
372
+ elif arg == "-tabs":
373
+ options.mode = "tabs"
374
+ elif arg == "-header":
375
+ options.header = True
376
+ elif arg == "-noheader":
377
+ options.header = False
378
+ elif arg == "-readonly":
379
+ options.readonly = True
380
+ elif arg == "-bail":
381
+ options.bail = True
382
+ elif arg == "-echo":
383
+ options.echo = True
384
+ elif arg == "-separator":
385
+ i += 1
386
+ if i >= len(args):
387
+ return ExecResult(
388
+ stdout="",
389
+ stderr="sqlite3: Error: missing argument to -separator\n",
390
+ exit_code=1,
391
+ )
392
+ options.separator = args[i]
393
+ elif arg == "-nullvalue":
394
+ i += 1
395
+ if i >= len(args):
396
+ return ExecResult(
397
+ stdout="",
398
+ stderr="sqlite3: Error: missing argument to -nullvalue\n",
399
+ exit_code=1,
400
+ )
401
+ options.null_value = args[i]
402
+ elif arg == "-cmd":
403
+ i += 1
404
+ if i >= len(args):
405
+ return ExecResult(
406
+ stdout="",
407
+ stderr="sqlite3: Error: missing argument to -cmd\n",
408
+ exit_code=1,
409
+ )
410
+ options.cmd = args[i]
411
+ elif arg.startswith("-"):
412
+ opt_name = arg[1:] if arg.startswith("--") else arg
413
+ return ExecResult(
414
+ stdout="",
415
+ stderr=f"sqlite3: Error: unknown option: {opt_name}\nUse -help for a list of options.\n",
416
+ exit_code=1,
417
+ )
418
+ elif database is None:
419
+ database = arg
420
+ elif sql_arg is None:
421
+ sql_arg = arg
422
+ i += 1
423
+
424
+ # Handle -version
425
+ if show_version:
426
+ version = sqlite3.sqlite_version
427
+ return ExecResult(stdout=f"{version}\n", stderr="", exit_code=0)
428
+
429
+ if not database:
430
+ return ExecResult(
431
+ stdout="",
432
+ stderr="sqlite3: missing database argument\n",
433
+ exit_code=1,
434
+ )
435
+
436
+ # Only support in-memory databases for now
437
+ if database != ":memory:":
438
+ return ExecResult(
439
+ stdout="",
440
+ stderr="sqlite3: only :memory: database is supported\n",
441
+ exit_code=1,
442
+ )
443
+
444
+ # Get SQL from argument or stdin
445
+ sql = sql_arg or ctx.stdin.strip()
446
+ if options.cmd:
447
+ sql = options.cmd + (";" + sql if sql else "")
448
+ if not sql:
449
+ return ExecResult(
450
+ stdout="",
451
+ stderr="sqlite3: no SQL provided\n",
452
+ exit_code=1,
453
+ )
454
+
455
+ # Execute SQL in memory
456
+ try:
457
+ conn = sqlite3.connect(":memory:")
458
+ cursor = conn.cursor()
459
+ stdout = ""
460
+
461
+ # Echo SQL if requested
462
+ if options.echo:
463
+ stdout += f"{sql}\n"
464
+
465
+ # Execute statements
466
+ statements = split_statements(sql)
467
+ had_error = False
468
+
469
+ for stmt in statements:
470
+ try:
471
+ cursor.execute(stmt)
472
+
473
+ # Fetch results for non-write statements
474
+ if not is_write_statement(stmt):
475
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
476
+ rows = cursor.fetchall()
477
+
478
+ if rows or options.header:
479
+ stdout += format_output(columns, rows, options)
480
+
481
+ except sqlite3.Error as e:
482
+ if options.bail:
483
+ conn.close()
484
+ return ExecResult(
485
+ stdout=stdout,
486
+ stderr=f"Error: {e}\n",
487
+ exit_code=1,
488
+ )
489
+ stdout += f"Error: {e}\n"
490
+ had_error = True
491
+
492
+ conn.close()
493
+
494
+ return ExecResult(
495
+ stdout=stdout,
496
+ stderr="",
497
+ exit_code=1 if had_error and options.bail else 0,
498
+ )
499
+
500
+ except Exception as e:
501
+ return ExecResult(
502
+ stdout="",
503
+ stderr=f"sqlite3: {e}\n",
504
+ exit_code=1,
505
+ )
@@ -0,0 +1,5 @@
1
+ """Stat command."""
2
+
3
+ from .stat import StatCommand
4
+
5
+ __all__ = ["StatCommand"]
@@ -0,0 +1,150 @@
1
+ """Stat command implementation."""
2
+
3
+ from datetime import datetime
4
+ from ...types import CommandContext, ExecResult
5
+
6
+
7
+ class StatCommand:
8
+ """The stat command."""
9
+
10
+ name = "stat"
11
+
12
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
13
+ """Execute the stat command."""
14
+ format_str = None
15
+ paths: list[str] = []
16
+
17
+ i = 0
18
+ while i < len(args):
19
+ arg = args[i]
20
+ if arg == "-c" and i + 1 < len(args):
21
+ i += 1
22
+ format_str = args[i]
23
+ elif arg.startswith("--format="):
24
+ format_str = arg[9:]
25
+ elif arg == "--help":
26
+ return ExecResult(
27
+ stdout="Usage: stat [OPTION]... FILE...\n",
28
+ stderr="",
29
+ exit_code=0,
30
+ )
31
+ elif arg == "--":
32
+ paths.extend(args[i + 1:])
33
+ break
34
+ elif arg.startswith("-") and len(arg) > 1:
35
+ return ExecResult(
36
+ stdout="",
37
+ stderr=f"stat: invalid option -- '{arg[1]}'\n",
38
+ exit_code=1,
39
+ )
40
+ else:
41
+ paths.append(arg)
42
+ i += 1
43
+
44
+ if not paths:
45
+ return ExecResult(
46
+ stdout="",
47
+ stderr="stat: missing operand\n",
48
+ exit_code=1,
49
+ )
50
+
51
+ stdout_parts = []
52
+ stderr = ""
53
+ exit_code = 0
54
+
55
+ for path in paths:
56
+ try:
57
+ resolved = ctx.fs.resolve_path(ctx.cwd, path)
58
+ stat = await ctx.fs.stat(resolved)
59
+
60
+ if format_str:
61
+ output = self._format(format_str, path, stat)
62
+ else:
63
+ output = self._default_format(path, stat)
64
+
65
+ stdout_parts.append(output)
66
+ except FileNotFoundError:
67
+ stderr += f"stat: cannot stat '{path}': No such file or directory\n"
68
+ exit_code = 1
69
+ except Exception as e:
70
+ stderr += f"stat: {path}: {e}\n"
71
+ exit_code = 1
72
+
73
+ return ExecResult(
74
+ stdout="\n".join(stdout_parts),
75
+ stderr=stderr,
76
+ exit_code=exit_code,
77
+ )
78
+
79
+ def _format(self, format_str: str, path: str, stat) -> str:
80
+ """Format stat output using format string."""
81
+ result = format_str
82
+
83
+ # %n - file name
84
+ result = result.replace("%n", path)
85
+
86
+ # %s - size
87
+ result = result.replace("%s", str(stat.size))
88
+
89
+ # %F - file type
90
+ if stat.is_directory:
91
+ file_type = "directory"
92
+ elif stat.is_symbolic_link:
93
+ file_type = "symbolic link"
94
+ else:
95
+ file_type = "regular file"
96
+ result = result.replace("%F", file_type)
97
+
98
+ # %a - access rights in octal
99
+ result = result.replace("%a", oct(stat.mode & 0o777)[2:])
100
+
101
+ # %A - access rights in human readable form
102
+ result = result.replace("%A", self._mode_to_string(stat.mode, stat.is_directory))
103
+
104
+ # %u - user ID (hardcoded for virtual FS)
105
+ result = result.replace("%u", "1000")
106
+
107
+ # %U - username (hardcoded for virtual FS)
108
+ result = result.replace("%U", "user")
109
+
110
+ # %g - group ID (hardcoded for virtual FS)
111
+ result = result.replace("%g", "1000")
112
+
113
+ # %G - group name (hardcoded for virtual FS)
114
+ result = result.replace("%G", "group")
115
+
116
+ return result + "\n"
117
+
118
+ def _default_format(self, path: str, stat) -> str:
119
+ """Generate default stat output."""
120
+ lines = []
121
+ lines.append(f" File: {path}")
122
+ lines.append(f" Size: {stat.size}")
123
+
124
+ if stat.is_directory:
125
+ file_type = "directory"
126
+ elif stat.is_symbolic_link:
127
+ file_type = "symbolic link"
128
+ else:
129
+ file_type = "regular file"
130
+
131
+ lines.append(f" Type: {file_type}")
132
+ lines.append(f"Access: ({oct(stat.mode & 0o777)[2:]}/{self._mode_to_string(stat.mode, stat.is_directory)})")
133
+
134
+ if stat.mtime:
135
+ mtime = datetime.fromtimestamp(stat.mtime)
136
+ lines.append(f"Modify: {mtime.isoformat()}")
137
+
138
+ return "\n".join(lines) + "\n"
139
+
140
+ def _mode_to_string(self, mode: int, is_dir: bool) -> str:
141
+ """Convert mode to rwx string."""
142
+ result = "d" if is_dir else "-"
143
+
144
+ for i in range(2, -1, -1):
145
+ bits = (mode >> (i * 3)) & 0o7
146
+ result += "r" if bits & 0o4 else "-"
147
+ result += "w" if bits & 0o2 else "-"
148
+ result += "x" if bits & 0o1 else "-"
149
+
150
+ return result
@@ -0,0 +1,5 @@
1
+ """Strings command."""
2
+
3
+ from .strings import StringsCommand
4
+
5
+ __all__ = ["StringsCommand"]