execsql2 2.23.0__py3-none-any.whl → 2.24.0__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.
- execsql/cli/__init__.py +17 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +233 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +451 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +513 -76
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +41 -14
- execsql/config.py +251 -154
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +24 -1
- execsql/data/script_header.sql +23 -0
- execsql/db/access.py +22 -6
- execsql/db/base.py +105 -1
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +95 -2
- execsql/db/factory.py +2 -2
- execsql/db/mysql.py +11 -0
- execsql/db/oracle.py +12 -4
- execsql/db/sqlite.py +16 -0
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/format.py +87 -75
- execsql/interactive.py +232 -0
- execsql/manifest.py +189 -0
- execsql/metacommands/conditions.py +1 -1
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/effects.py +124 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +6 -93
- execsql/script/executor.py +153 -34
- execsql/state.py +11 -2
- execsql/state.pyi +3 -0
- execsql/utils/color.py +18 -0
- execsql/utils/errors.py +11 -1
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.0.dist-info → execsql2-2.24.0.dist-info}/METADATA +111 -58
- {execsql2-2.23.0.dist-info → execsql2-2.24.0.dist-info}/RECORD +62 -46
- execsql2-2.24.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.0.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.24.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.24.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.24.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""The ``run`` command: execute one script against a database."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from execsql.cli.application import ExecsqlCommand, _online_help_callback, _version_callback, app
|
|
10
|
+
from execsql.cli.help import (
|
|
11
|
+
_console,
|
|
12
|
+
_err_console,
|
|
13
|
+
_init_config,
|
|
14
|
+
_print_encodings,
|
|
15
|
+
_print_keywords_json,
|
|
16
|
+
_print_metacommands,
|
|
17
|
+
_print_plugins,
|
|
18
|
+
)
|
|
19
|
+
from execsql.cli.options import (
|
|
20
|
+
ConfigFileOpt,
|
|
21
|
+
DbTypeOpt,
|
|
22
|
+
DsnOpt,
|
|
23
|
+
NoPasswdOpt,
|
|
24
|
+
PortOpt,
|
|
25
|
+
ScriptEncodingOpt,
|
|
26
|
+
UserOpt,
|
|
27
|
+
)
|
|
28
|
+
from execsql.cli.run import _run
|
|
29
|
+
from execsql.exceptions import ErrInfo
|
|
30
|
+
|
|
31
|
+
__all__ = ["main"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command(
|
|
35
|
+
cls=ExecsqlCommand,
|
|
36
|
+
name="run",
|
|
37
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
38
|
+
)
|
|
39
|
+
def main(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
# Positional args collected manually (script + optional server/db/file)
|
|
42
|
+
args: list[str] | None = typer.Argument(
|
|
43
|
+
None,
|
|
44
|
+
metavar="[SQL_SCRIPT [SERVER DATABASE | DATABASE_FILE]]",
|
|
45
|
+
help=(
|
|
46
|
+
"SQL script file to execute. Optionally followed by server and database "
|
|
47
|
+
"name (client-server DBs) or a database file path (file-based DBs). "
|
|
48
|
+
"Not needed with -c or --ping."
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
# -- Connection --------------------------------------------------------
|
|
52
|
+
db_type: DbTypeOpt = None,
|
|
53
|
+
dsn: DsnOpt = None,
|
|
54
|
+
user: UserOpt = None,
|
|
55
|
+
port: PortOpt = None,
|
|
56
|
+
no_passwd: NoPasswdOpt = False,
|
|
57
|
+
new_db: bool = typer.Option(
|
|
58
|
+
False,
|
|
59
|
+
"-n",
|
|
60
|
+
"--new-db",
|
|
61
|
+
help="Create a new SQLite or Postgres database if it does not exist.",
|
|
62
|
+
),
|
|
63
|
+
# -- Encoding ----------------------------------------------------------
|
|
64
|
+
database_encoding: str | None = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
"-e",
|
|
67
|
+
"--database-encoding",
|
|
68
|
+
help="Character encoding used in the database.",
|
|
69
|
+
),
|
|
70
|
+
script_encoding: ScriptEncodingOpt = None,
|
|
71
|
+
output_encoding: str | None = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"-g",
|
|
74
|
+
"--output-encoding",
|
|
75
|
+
help="Encoding for WRITE and EXPORT output.",
|
|
76
|
+
),
|
|
77
|
+
import_encoding: str | None = typer.Option(
|
|
78
|
+
None,
|
|
79
|
+
"-i",
|
|
80
|
+
"--import-encoding",
|
|
81
|
+
help="Encoding for data files used with IMPORT.",
|
|
82
|
+
),
|
|
83
|
+
# -- Import/Export -----------------------------------------------------
|
|
84
|
+
import_buffer: int | None = typer.Option(
|
|
85
|
+
None,
|
|
86
|
+
"-z",
|
|
87
|
+
"--import-buffer",
|
|
88
|
+
metavar="KB",
|
|
89
|
+
help="Import buffer size in KB. Default: 32",
|
|
90
|
+
),
|
|
91
|
+
scanlines: int | None = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"-s",
|
|
94
|
+
"--scan-lines",
|
|
95
|
+
metavar="N",
|
|
96
|
+
help="Lines to scan for IMPORT format detection. 0 = scan entire file.",
|
|
97
|
+
),
|
|
98
|
+
boolean_int: str | None = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"-b",
|
|
101
|
+
"--boolean-int",
|
|
102
|
+
metavar="{0,1,t,f,y,n}",
|
|
103
|
+
help="Treat integers 0 and 1 as boolean values.",
|
|
104
|
+
),
|
|
105
|
+
make_dirs: str | None = typer.Option(
|
|
106
|
+
None,
|
|
107
|
+
"-d",
|
|
108
|
+
"--directories",
|
|
109
|
+
metavar="{0,1,t,f,y,n}",
|
|
110
|
+
help="Auto-create directories for EXPORT metacommand. n=no (default), y=yes",
|
|
111
|
+
),
|
|
112
|
+
output_dir: str | None = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--output-dir",
|
|
115
|
+
metavar="DIR",
|
|
116
|
+
help=(
|
|
117
|
+
"Default base directory for EXPORT output files. "
|
|
118
|
+
"Relative paths in EXPORT metacommands are joined to this directory. "
|
|
119
|
+
"Absolute paths and stdout are unaffected."
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
progress: bool = typer.Option(
|
|
123
|
+
False,
|
|
124
|
+
"--progress",
|
|
125
|
+
help="Show a progress bar for long-running IMPORT operations.",
|
|
126
|
+
),
|
|
127
|
+
# -- Execution ---------------------------------------------------------
|
|
128
|
+
command: str | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"-c",
|
|
131
|
+
"--command",
|
|
132
|
+
metavar="SCRIPT",
|
|
133
|
+
help=(
|
|
134
|
+
"Execute an inline SQL/metacommand script string instead of a script file. "
|
|
135
|
+
"Use shell $'line1\\nline2' syntax for multi-line scripts."
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
dry_run: bool = typer.Option(
|
|
139
|
+
False,
|
|
140
|
+
"--dry-run",
|
|
141
|
+
help="Parse the script and print the command list without connecting to a database or executing anything.",
|
|
142
|
+
),
|
|
143
|
+
# Removed: `execsql lint` replaces it. Still declared so the flag is
|
|
144
|
+
# refused with a pointer instead of Click's generic "no such option".
|
|
145
|
+
lint: bool = typer.Option(False, "--lint", hidden=True),
|
|
146
|
+
parse_tree: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
"--parse-tree",
|
|
149
|
+
help=(
|
|
150
|
+
"Parse the script into an abstract syntax tree and print the tree structure. "
|
|
151
|
+
"Does not connect to a database or execute anything."
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
debug: bool = typer.Option(
|
|
155
|
+
False,
|
|
156
|
+
"--debug",
|
|
157
|
+
help="Start in step-through debug mode. The debug REPL pauses before each statement.",
|
|
158
|
+
),
|
|
159
|
+
no_system_cmd: bool = typer.Option(
|
|
160
|
+
False,
|
|
161
|
+
"--no-system-cmd",
|
|
162
|
+
help="Disable the SYSTEM_CMD (SHELL) metacommand. Scripts that use SHELL will fail with an error.",
|
|
163
|
+
),
|
|
164
|
+
no_rm_file: bool = typer.Option(
|
|
165
|
+
False,
|
|
166
|
+
"--no-rm-file",
|
|
167
|
+
help="Disable the RM_FILE metacommand. Scripts that try to delete files will fail with an error.",
|
|
168
|
+
),
|
|
169
|
+
no_serve: bool = typer.Option(
|
|
170
|
+
False,
|
|
171
|
+
"--no-serve",
|
|
172
|
+
help="Disable the SERVE metacommand. Scripts that try to stream a file to stdout will fail with an error.",
|
|
173
|
+
),
|
|
174
|
+
profile: bool = typer.Option(
|
|
175
|
+
False,
|
|
176
|
+
"--profile",
|
|
177
|
+
help="Record per-statement execution times and print a timing summary after the script completes.",
|
|
178
|
+
),
|
|
179
|
+
profile_limit: int = typer.Option(
|
|
180
|
+
20,
|
|
181
|
+
"--profile-limit",
|
|
182
|
+
help="Number of top statements to show in the --profile timing summary (default: 20).",
|
|
183
|
+
),
|
|
184
|
+
# -- GUI ---------------------------------------------------------------
|
|
185
|
+
use_gui: str | None = typer.Option(
|
|
186
|
+
None,
|
|
187
|
+
"-v",
|
|
188
|
+
"--visible-prompts",
|
|
189
|
+
metavar="{0,1,2,3}",
|
|
190
|
+
help=(
|
|
191
|
+
"GUI level: 0=none (default), 1=GUI for password/pause, "
|
|
192
|
+
"2=GUI for password/pause + DB selection, 3=full GUI console."
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
gui_framework: str | None = typer.Option(
|
|
196
|
+
None,
|
|
197
|
+
"--gui-framework",
|
|
198
|
+
metavar="{tkinter,textual}",
|
|
199
|
+
help="GUI framework to use with --visible-prompts. Default: tkinter",
|
|
200
|
+
),
|
|
201
|
+
# -- Configuration -----------------------------------------------------
|
|
202
|
+
config_file: ConfigFileOpt = None,
|
|
203
|
+
init_config: bool = typer.Option(False, "--init-config", hidden=True), # alias: execsql config --init
|
|
204
|
+
sub_vars: list[str] | None = typer.Option(
|
|
205
|
+
None,
|
|
206
|
+
"-a",
|
|
207
|
+
"--assign-arg",
|
|
208
|
+
metavar="VALUE",
|
|
209
|
+
help="Define the replacement string for a substitution variable $ARG_x.",
|
|
210
|
+
),
|
|
211
|
+
named_vars: list[str] | None = typer.Option(
|
|
212
|
+
None,
|
|
213
|
+
"--var",
|
|
214
|
+
metavar="NAME=VALUE",
|
|
215
|
+
help=(
|
|
216
|
+
"Set the substitution variable !!NAME!!, as a [variables] entry in a config file would; "
|
|
217
|
+
"it wins over config files, and a SUB in the script can reassign it. Repeatable."
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
manifest_path: str | None = typer.Option(
|
|
221
|
+
None,
|
|
222
|
+
"--manifest",
|
|
223
|
+
metavar="FILE",
|
|
224
|
+
help=(
|
|
225
|
+
"Write a JSON record of the run to FILE when it ends, even if it fails: script, database, "
|
|
226
|
+
"statement counts, files read, written and deleted, connections, errors. No values or passwords."
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
user_logfile: bool = typer.Option(
|
|
230
|
+
False,
|
|
231
|
+
"-l",
|
|
232
|
+
"--user-logfile",
|
|
233
|
+
help="Write a log file to ~/execsql.log.",
|
|
234
|
+
),
|
|
235
|
+
# -- Hidden aliases of `execsql list ...`; -m and -y are upstream flags.
|
|
236
|
+
metacommands: bool = typer.Option(False, "-m", "--metacommands", hidden=True), # list metacommands
|
|
237
|
+
encodings: bool = typer.Option(False, "-y", "--encodings", hidden=True), # list encodings
|
|
238
|
+
dump_keywords: bool = typer.Option(False, "--dump-keywords", hidden=True), # list keywords --output-format json
|
|
239
|
+
list_plugins: bool = typer.Option(False, "--list-plugins", hidden=True), # list plugins
|
|
240
|
+
ping: bool = typer.Option(
|
|
241
|
+
False,
|
|
242
|
+
"--ping",
|
|
243
|
+
help=(
|
|
244
|
+
"Test database connectivity and exit. Prints connection details and the server version on success "
|
|
245
|
+
"(exit 0), or the error message on failure (exit 1). No script file is required."
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
# The global options again, hidden. Upstream's optparse accepted them
|
|
249
|
+
# anywhere, so `execsql script.sql db --version` still has to print the
|
|
250
|
+
# version rather than read "--version" as a database name.
|
|
251
|
+
_online_help: bool = typer.Option(
|
|
252
|
+
False,
|
|
253
|
+
"-o",
|
|
254
|
+
"--online-help",
|
|
255
|
+
callback=_online_help_callback,
|
|
256
|
+
is_eager=True,
|
|
257
|
+
hidden=True,
|
|
258
|
+
),
|
|
259
|
+
_version: bool | None = typer.Option(
|
|
260
|
+
None,
|
|
261
|
+
"--version",
|
|
262
|
+
callback=_version_callback,
|
|
263
|
+
is_eager=True,
|
|
264
|
+
hidden=True,
|
|
265
|
+
),
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Run a script against a database.
|
|
268
|
+
|
|
269
|
+
\b
|
|
270
|
+
Client-server databases: execsql run script.sql SERVER DATABASE
|
|
271
|
+
File-based databases: execsql run script.sql DATABASE_FILE
|
|
272
|
+
An inline script: execsql run -c "select 1;" DATABASE_FILE
|
|
273
|
+
A connection test: execsql run --ping DATABASE_FILE
|
|
274
|
+
|
|
275
|
+
Running a script without run (execsql script.sql ...) is deprecated and
|
|
276
|
+
stops working in execsql2 3.0.
|
|
277
|
+
"""
|
|
278
|
+
if lint:
|
|
279
|
+
_err_console.print("[bold red]Error:[/bold red] run --lint was removed; use execsql lint")
|
|
280
|
+
raise typer.Exit(code=2)
|
|
281
|
+
|
|
282
|
+
# ------------------------------------------------------------------
|
|
283
|
+
# Information flags and the --init-config alias (no script file needed)
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
if metacommands:
|
|
286
|
+
_print_metacommands()
|
|
287
|
+
raise typer.Exit()
|
|
288
|
+
|
|
289
|
+
if encodings:
|
|
290
|
+
_print_encodings()
|
|
291
|
+
raise typer.Exit()
|
|
292
|
+
|
|
293
|
+
if init_config:
|
|
294
|
+
_init_config()
|
|
295
|
+
raise typer.Exit()
|
|
296
|
+
|
|
297
|
+
if dump_keywords:
|
|
298
|
+
_print_keywords_json()
|
|
299
|
+
raise typer.Exit()
|
|
300
|
+
|
|
301
|
+
if list_plugins:
|
|
302
|
+
_print_plugins()
|
|
303
|
+
raise typer.Exit()
|
|
304
|
+
|
|
305
|
+
parsed_vars = _parse_named_vars(named_vars)
|
|
306
|
+
|
|
307
|
+
if config_file and not Path(config_file).is_file():
|
|
308
|
+
_err_console.print(
|
|
309
|
+
f"[bold red]Error:[/bold red] Config file {config_file!r} does not exist.",
|
|
310
|
+
)
|
|
311
|
+
raise typer.Exit(code=2)
|
|
312
|
+
|
|
313
|
+
if manifest_path and not dry_run:
|
|
314
|
+
from execsql.manifest import check_writable
|
|
315
|
+
|
|
316
|
+
if (problem := check_writable(manifest_path)) is not None:
|
|
317
|
+
_err_console.print(f"[bold red]Error:[/bold red] Cannot write --manifest {manifest_path}: {problem}")
|
|
318
|
+
raise typer.Exit(code=2)
|
|
319
|
+
|
|
320
|
+
positional = args or []
|
|
321
|
+
if command is not None:
|
|
322
|
+
script_name = None # inline mode — no script file
|
|
323
|
+
elif ping:
|
|
324
|
+
# --ping does not require a script file; positional args are still
|
|
325
|
+
# available for server/db arguments if --dsn is not used.
|
|
326
|
+
script_name = None
|
|
327
|
+
else:
|
|
328
|
+
if not positional:
|
|
329
|
+
_err_console.print(
|
|
330
|
+
"[bold red]Error:[/bold red] No SQL script file specified. Use -c to run an inline script.",
|
|
331
|
+
)
|
|
332
|
+
raise typer.Exit(code=1)
|
|
333
|
+
script_name = positional[0]
|
|
334
|
+
if not Path(script_name).exists():
|
|
335
|
+
_err_console.print(
|
|
336
|
+
f'[bold red]Error:[/bold red] SQL script file "{script_name}" does not exist.',
|
|
337
|
+
)
|
|
338
|
+
raise typer.Exit(code=1)
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
# Validate positional args and db_type choice
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
if db_type and db_type not in ("a", "d", "p", "s", "l", "m", "k", "o", "f"):
|
|
345
|
+
_err_console.print(
|
|
346
|
+
f"[bold red]Error:[/bold red] Invalid database type {db_type!r}. Choose from: a, d, p, s, l, m, k, o, f",
|
|
347
|
+
)
|
|
348
|
+
raise typer.Exit(code=2)
|
|
349
|
+
|
|
350
|
+
if use_gui and use_gui not in ("0", "1", "2", "3"):
|
|
351
|
+
_err_console.print(
|
|
352
|
+
f"[bold red]Error:[/bold red] Invalid GUI level {use_gui!r}. Choose from: 0, 1, 2, 3",
|
|
353
|
+
)
|
|
354
|
+
raise typer.Exit(code=2)
|
|
355
|
+
|
|
356
|
+
if gui_framework and gui_framework.lower() not in ("tkinter", "textual"):
|
|
357
|
+
_err_console.print(
|
|
358
|
+
f"[bold red]Error:[/bold red] Invalid GUI framework {gui_framework!r}. Choose from: tkinter, textual",
|
|
359
|
+
)
|
|
360
|
+
raise typer.Exit(code=2)
|
|
361
|
+
|
|
362
|
+
if boolean_int and boolean_int.lower() not in ("0", "1", "t", "f", "y", "n"):
|
|
363
|
+
_err_console.print(
|
|
364
|
+
f"[bold red]Error:[/bold red] Invalid --boolean-int value {boolean_int!r}.",
|
|
365
|
+
)
|
|
366
|
+
raise typer.Exit(code=2)
|
|
367
|
+
|
|
368
|
+
# ------------------------------------------------------------------
|
|
369
|
+
# Parse tree: parse script into AST and print tree structure
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
if parse_tree:
|
|
372
|
+
from execsql.script.ast import format_tree
|
|
373
|
+
from execsql.script.parser import parse_script, parse_string
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
if command is not None:
|
|
377
|
+
tree = parse_string(command.replace("\\n", "\n").replace("\\t", "\t"), "<inline>")
|
|
378
|
+
elif script_name is not None:
|
|
379
|
+
encoding = script_encoding or "utf-8"
|
|
380
|
+
tree = parse_script(script_name, encoding=encoding)
|
|
381
|
+
else:
|
|
382
|
+
_err_console.print(
|
|
383
|
+
"[bold red]Error:[/bold red] --parse-tree requires a script file or -c command.",
|
|
384
|
+
)
|
|
385
|
+
raise typer.Exit(code=1)
|
|
386
|
+
except ErrInfo as exc:
|
|
387
|
+
_err_console.print(f"[bold red]Parse error:[/bold red] {exc.errmsg()}")
|
|
388
|
+
raise typer.Exit(code=1) from exc
|
|
389
|
+
|
|
390
|
+
_console.print(format_tree(tree))
|
|
391
|
+
raise typer.Exit()
|
|
392
|
+
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
# Delegate to the real main implementation
|
|
395
|
+
# ------------------------------------------------------------------
|
|
396
|
+
_run(
|
|
397
|
+
positional=positional,
|
|
398
|
+
sub_vars=sub_vars,
|
|
399
|
+
boolean_int=boolean_int,
|
|
400
|
+
make_dirs=make_dirs,
|
|
401
|
+
database_encoding=database_encoding,
|
|
402
|
+
script_encoding=script_encoding,
|
|
403
|
+
output_encoding=output_encoding,
|
|
404
|
+
import_encoding=import_encoding,
|
|
405
|
+
user_logfile=user_logfile,
|
|
406
|
+
new_db=new_db,
|
|
407
|
+
port=port,
|
|
408
|
+
scanlines=scanlines,
|
|
409
|
+
db_type=db_type,
|
|
410
|
+
user=user,
|
|
411
|
+
use_gui=use_gui,
|
|
412
|
+
gui_framework=gui_framework,
|
|
413
|
+
no_passwd=no_passwd,
|
|
414
|
+
import_buffer=import_buffer,
|
|
415
|
+
script_name=script_name,
|
|
416
|
+
command=command,
|
|
417
|
+
dry_run=dry_run,
|
|
418
|
+
dsn=dsn,
|
|
419
|
+
output_dir=output_dir,
|
|
420
|
+
progress=progress,
|
|
421
|
+
profile=profile,
|
|
422
|
+
profile_limit=profile_limit,
|
|
423
|
+
ping=ping,
|
|
424
|
+
debug=debug,
|
|
425
|
+
no_system_cmd=no_system_cmd,
|
|
426
|
+
no_rm_file=no_rm_file,
|
|
427
|
+
no_serve=no_serve,
|
|
428
|
+
config_file=config_file,
|
|
429
|
+
named_vars=parsed_vars,
|
|
430
|
+
manifest_path=manifest_path,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _parse_named_vars(values: list[str] | None) -> list[tuple[str, str]]:
|
|
435
|
+
"""``--var NAME=VALUE`` pairs, in order. Rejects a missing ``=`` or a name execsql reserves."""
|
|
436
|
+
import re
|
|
437
|
+
|
|
438
|
+
pairs: list[tuple[str, str]] = []
|
|
439
|
+
for raw in values or ():
|
|
440
|
+
name, sep, value = raw.partition("=")
|
|
441
|
+
name = name.strip()
|
|
442
|
+
if not sep:
|
|
443
|
+
raise typer.BadParameter(f"{raw!r} has no '='; write NAME=VALUE.", param_hint="'--var'")
|
|
444
|
+
if not re.fullmatch(r"\w+", name):
|
|
445
|
+
raise typer.BadParameter(
|
|
446
|
+
f"{name!r} is not a variable name: use letters, digits and _ only. "
|
|
447
|
+
"Names starting with $, & or @ are execsql's own (system, environment and column variables).",
|
|
448
|
+
param_hint="'--var'",
|
|
449
|
+
)
|
|
450
|
+
pairs.append((name, value))
|
|
451
|
+
return pairs
|
execsql/cli/dispatch.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Command dispatch for the ``execsql`` console script.
|
|
2
|
+
|
|
3
|
+
execsql is six commands behind one program — ``run``, ``format``, ``lint``,
|
|
4
|
+
``config``, ``list`` and ``init`` — each a real :class:`typer.Typer` command,
|
|
5
|
+
so ``execsql --help`` lists them the way any multi-command CLI does.
|
|
6
|
+
|
|
7
|
+
What this module adds on top is one rule, applied before the parser sees
|
|
8
|
+
anything: **an argument list with no command in it means** ``run``.
|
|
9
|
+
``execsql script.sql server db`` has been the invocation since upstream
|
|
10
|
+
v1.130.1 — it is in shell scripts and cron entries — so :func:`normalize`
|
|
11
|
+
still inserts the verb. That form is deprecated: the app warns when it
|
|
12
|
+
inserts ``run``, and the form stops working in execsql2 3.0.
|
|
13
|
+
|
|
14
|
+
Two things must not have ``run`` inserted: a command name, and an option the
|
|
15
|
+
app declares itself — ``--help``, ``--version``, ``--online-help``.
|
|
16
|
+
Everything else, including ``-m`` and the other hidden aliases declared on
|
|
17
|
+
``run``, is a run.
|
|
18
|
+
|
|
19
|
+
A command name is always a command, whatever files the working directory
|
|
20
|
+
holds: a linter must never be able to execute a script because a file named
|
|
21
|
+
``lint`` happens to be present. A script named exactly like a command, with
|
|
22
|
+
no extension, runs with ``execsql run NAME``. That is a
|
|
23
|
+
recorded, maintainer-approved break from upstream (docs/about/divergence.md).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["COMMANDS", "GLOBAL_FLAGS", "dispatch", "normalize"]
|
|
30
|
+
|
|
31
|
+
#: Tokens that name a command.
|
|
32
|
+
COMMANDS = ("run", "format", "lint", "config", "list", "init")
|
|
33
|
+
|
|
34
|
+
#: Options declared on the app rather than on a command, which must reach
|
|
35
|
+
#: the parser without ``run`` in front of them. ``-m``, ``--encodings`` and
|
|
36
|
+
#: the other early-exit options are declared on ``run``, so they are not
|
|
37
|
+
#: here — they get the verb inserted like any other run invocation.
|
|
38
|
+
GLOBAL_FLAGS = ("--help", "-h", "--version", "-o", "--online-help")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize(argv: list[str]) -> list[str]:
|
|
42
|
+
"""Insert ``run`` when *argv* carries no command.
|
|
43
|
+
|
|
44
|
+
Takes and returns the arguments after the program name. *argv* itself is
|
|
45
|
+
returned when nothing is inserted, so a caller can tell the deprecated
|
|
46
|
+
bare form by identity.
|
|
47
|
+
"""
|
|
48
|
+
if not argv:
|
|
49
|
+
return argv
|
|
50
|
+
head = argv[0]
|
|
51
|
+
if head in COMMANDS or head in GLOBAL_FLAGS:
|
|
52
|
+
return argv
|
|
53
|
+
return ["run", *argv]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dispatch() -> None:
|
|
57
|
+
"""Console-script entry point.
|
|
58
|
+
|
|
59
|
+
:func:`normalize` is applied by the app's command group, so every caller —
|
|
60
|
+
this entry point, ``python -m execsql``, and a test driving ``app``
|
|
61
|
+
directly — gets the same treatment. This is a thin alias kept because
|
|
62
|
+
pyproject names it.
|
|
63
|
+
"""
|
|
64
|
+
from execsql.cli import _legacy_main
|
|
65
|
+
|
|
66
|
+
_legacy_main()
|