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,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,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
|