mycli 2.15.0__tar.gz → 2.16.0__tar.gz

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 (190) hide show
  1. {mycli-2.15.0/mycli.egg-info → mycli-2.16.0}/PKG-INFO +1 -1
  2. {mycli-2.15.0 → mycli-2.16.0}/changelog.md +17 -0
  3. {mycli-2.15.0 → mycli-2.16.0}/mycli/TIPS +4 -4
  4. {mycli-2.15.0 → mycli-2.16.0}/mycli/client_commands.py +167 -7
  5. {mycli-2.15.0 → mycli-2.16.0}/mycli/client_query.py +9 -0
  6. {mycli-2.15.0 → mycli-2.16.0}/mycli/main.py +0 -1
  7. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/execute.py +1 -1
  8. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/repl.py +146 -1
  9. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/completion_engine.py +38 -0
  10. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/filepaths.py +1 -1
  11. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/iocommands.py +2 -2
  12. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/main.py +1 -1
  13. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/sqlresult.py +3 -1
  14. {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/bash/mycli +1 -1
  15. {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/fish/mycli.fish +1 -1
  16. {mycli-2.15.0 → mycli-2.16.0}/mycli/resources/completions/zsh/_mycli +1 -1
  17. {mycli-2.15.0 → mycli-2.16.0}/mycli/sqlcompleter.py +40 -1
  18. {mycli-2.15.0 → mycli-2.16.0/mycli.egg-info}/PKG-INFO +1 -1
  19. mycli-2.16.0/mycli.egg-info/scm_version.json +8 -0
  20. mycli-2.16.0/test/features/fixture_data/help_commands.txt +42 -0
  21. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_commands.py +345 -3
  22. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_query.py +41 -0
  23. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completion_engine.py +37 -4
  24. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_filepaths.py +1 -0
  25. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main.py +2 -2
  26. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_execute.py +6 -6
  27. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_repl.py +342 -0
  28. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_smart_completion_public_schema_only.py +76 -9
  29. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_main.py +1 -1
  30. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlresult.py +2 -0
  31. mycli-2.15.0/mycli.egg-info/scm_version.json +0 -8
  32. mycli-2.15.0/test/features/fixture_data/help_commands.txt +0 -42
  33. {mycli-2.15.0 → mycli-2.16.0}/.git-blame-ignore-revs +0 -0
  34. {mycli-2.15.0 → mycli-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  35. {mycli-2.15.0 → mycli-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  36. {mycli-2.15.0 → mycli-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {mycli-2.15.0 → mycli-2.16.0}/.github/dependabot.yml +0 -0
  38. {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/ci.yml +0 -0
  39. {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/lint.yml +0 -0
  40. {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/publish.yml +0 -0
  41. {mycli-2.15.0 → mycli-2.16.0}/.github/workflows/typecheck.yml +0 -0
  42. {mycli-2.15.0 → mycli-2.16.0}/.gitignore +0 -0
  43. {mycli-2.15.0 → mycli-2.16.0}/AGENTS.md +0 -0
  44. {mycli-2.15.0 → mycli-2.16.0}/AUTHORS.rst +0 -0
  45. {mycli-2.15.0 → mycli-2.16.0}/CONTRIBUTING.md +0 -0
  46. {mycli-2.15.0 → mycli-2.16.0}/LICENSE.txt +0 -0
  47. {mycli-2.15.0 → mycli-2.16.0}/MANIFEST.in +0 -0
  48. {mycli-2.15.0 → mycli-2.16.0}/README.md +0 -0
  49. {mycli-2.15.0 → mycli-2.16.0}/SPONSORS.rst +0 -0
  50. {mycli-2.15.0 → mycli-2.16.0}/doc/key_bindings.rst +0 -0
  51. {mycli-2.15.0 → mycli-2.16.0}/doc/llm.md +0 -0
  52. {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/main.gif +0 -0
  53. {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/tables.png +0 -0
  54. {mycli-2.15.0 → mycli-2.16.0}/doc/screenshots/total_histogram.png +0 -0
  55. {mycli-2.15.0 → mycli-2.16.0}/doc/transforms.md +0 -0
  56. {mycli-2.15.0 → mycli-2.16.0}/mycli/AUTHORS +0 -0
  57. {mycli-2.15.0 → mycli-2.16.0}/mycli/SPONSORS +0 -0
  58. {mycli-2.15.0 → mycli-2.16.0}/mycli/__init__.py +0 -0
  59. {mycli-2.15.0 → mycli-2.16.0}/mycli/app_state.py +0 -0
  60. {mycli-2.15.0 → mycli-2.16.0}/mycli/boundary_tunnel.py +0 -0
  61. {mycli-2.15.0 → mycli-2.16.0}/mycli/cli_runner.py +0 -0
  62. {mycli-2.15.0 → mycli-2.16.0}/mycli/clibuffer.py +0 -0
  63. {mycli-2.15.0 → mycli-2.16.0}/mycli/client.py +0 -0
  64. {mycli-2.15.0 → mycli-2.16.0}/mycli/client_connection.py +0 -0
  65. {mycli-2.15.0 → mycli-2.16.0}/mycli/clistyle.py +0 -0
  66. {mycli-2.15.0 → mycli-2.16.0}/mycli/clitoolbar.py +0 -0
  67. {mycli-2.15.0 → mycli-2.16.0}/mycli/compat.py +0 -0
  68. {mycli-2.15.0 → mycli-2.16.0}/mycli/completion_refresher.py +0 -0
  69. {mycli-2.15.0 → mycli-2.16.0}/mycli/config.py +0 -0
  70. {mycli-2.15.0 → mycli-2.16.0}/mycli/constants.py +0 -0
  71. {mycli-2.15.0 → mycli-2.16.0}/mycli/key_bindings.py +0 -0
  72. {mycli-2.15.0 → mycli-2.16.0}/mycli/lexer.py +0 -0
  73. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/batch.py +0 -0
  74. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/checkup.py +0 -0
  75. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/completions.py +0 -0
  76. {mycli-2.15.0 → mycli-2.16.0}/mycli/main_modes/list_dsn.py +0 -0
  77. {mycli-2.15.0 → mycli-2.16.0}/mycli/myclirc +0 -0
  78. {mycli-2.15.0 → mycli-2.16.0}/mycli/output.py +0 -0
  79. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/__init__.py +0 -0
  80. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/batch_utils.py +0 -0
  81. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/cli_utils.py +0 -0
  82. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/hybrid_redirection.py +0 -0
  83. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/interactive_utils.py +0 -0
  84. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/key_binding_utils.py +0 -0
  85. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/polars_transform.py +0 -0
  86. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  87. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  88. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/history.py +0 -0
  89. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/ptoolkit/utils.py +0 -0
  90. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/__init__.py +0 -0
  91. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/dbcommands.py +0 -0
  92. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/delimitercommand.py +0 -0
  93. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/dsn_aliases.py +0 -0
  94. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/favoritequeries.py +0 -0
  95. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/llm.py +0 -0
  96. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/special/utils.py +0 -0
  97. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/sql_utils.py +0 -0
  98. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/string_utils.py +0 -0
  99. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/tabular_output/__init__.py +0 -0
  100. {mycli-2.15.0 → mycli-2.16.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  101. {mycli-2.15.0 → mycli-2.16.0}/mycli/password_sources.py +0 -0
  102. {mycli-2.15.0 → mycli-2.16.0}/mycli/schema_prefetcher.py +0 -0
  103. {mycli-2.15.0 → mycli-2.16.0}/mycli/sqlexecute.py +0 -0
  104. {mycli-2.15.0 → mycli-2.16.0}/mycli/ssh_tunnel.py +0 -0
  105. {mycli-2.15.0 → mycli-2.16.0}/mycli/types.py +0 -0
  106. {mycli-2.15.0 → mycli-2.16.0}/mycli/vault.py +0 -0
  107. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/SOURCES.txt +0 -0
  108. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/dependency_links.txt +0 -0
  109. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/entry_points.txt +0 -0
  110. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/requires.txt +0 -0
  111. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/scm_file_list.json +0 -0
  112. {mycli-2.15.0 → mycli-2.16.0}/mycli.egg-info/top_level.txt +0 -0
  113. {mycli-2.15.0 → mycli-2.16.0}/pyproject.toml +0 -0
  114. {mycli-2.15.0 → mycli-2.16.0}/setup.cfg +0 -0
  115. {mycli-2.15.0 → mycli-2.16.0}/test/__init__.py +0 -0
  116. {mycli-2.15.0 → mycli-2.16.0}/test/features/__init__.py +0 -0
  117. {mycli-2.15.0 → mycli-2.16.0}/test/features/auto_vertical.feature +0 -0
  118. {mycli-2.15.0 → mycli-2.16.0}/test/features/basic_commands.feature +0 -0
  119. {mycli-2.15.0 → mycli-2.16.0}/test/features/connection.feature +0 -0
  120. {mycli-2.15.0 → mycli-2.16.0}/test/features/crud_database.feature +0 -0
  121. {mycli-2.15.0 → mycli-2.16.0}/test/features/crud_table.feature +0 -0
  122. {mycli-2.15.0 → mycli-2.16.0}/test/features/db_utils.py +0 -0
  123. {mycli-2.15.0 → mycli-2.16.0}/test/features/environment.py +0 -0
  124. {mycli-2.15.0 → mycli-2.16.0}/test/features/fixture_utils.py +0 -0
  125. {mycli-2.15.0 → mycli-2.16.0}/test/features/iocommands.feature +0 -0
  126. {mycli-2.15.0 → mycli-2.16.0}/test/features/named_queries.feature +0 -0
  127. {mycli-2.15.0 → mycli-2.16.0}/test/features/specials.feature +0 -0
  128. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/__init__.py +0 -0
  129. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/auto_vertical.py +0 -0
  130. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/basic_commands.py +0 -0
  131. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/connection.py +0 -0
  132. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/crud_database.py +0 -0
  133. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/crud_table.py +0 -0
  134. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/iocommands.py +0 -0
  135. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/named_queries.py +0 -0
  136. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/specials.py +0 -0
  137. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/utils.py +0 -0
  138. {mycli-2.15.0 → mycli-2.16.0}/test/features/steps/wrappers.py +0 -0
  139. {mycli-2.15.0 → mycli-2.16.0}/test/features/wrappager.py +0 -0
  140. {mycli-2.15.0 → mycli-2.16.0}/test/myclirc +0 -0
  141. {mycli-2.15.0 → mycli-2.16.0}/test/mylogin.cnf +0 -0
  142. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/conftest.py +0 -0
  143. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_app_state.py +0 -0
  144. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_bash_completion.py +0 -0
  145. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_batch_utils.py +0 -0
  146. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_boundary_tunnel.py +0 -0
  147. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_checkup.py +0 -0
  148. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_cli_runner.py +0 -0
  149. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_cli_utils.py +0 -0
  150. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clibuffer.py +0 -0
  151. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client.py +0 -0
  152. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_client_connection.py +0 -0
  153. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clistyle.py +0 -0
  154. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_clitoolbar.py +0 -0
  155. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completer_use_switch.py +0 -0
  156. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_completion_refresher.py +0 -0
  157. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_config.py +0 -0
  158. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_delimitercommand.py +0 -0
  159. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_dsn_aliases.py +0 -0
  160. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_favoritequeries.py +0 -0
  161. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_fish_completion.py +0 -0
  162. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_hybrid_redirection.py +0 -0
  163. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_interactive_utils.py +0 -0
  164. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_key_binding_utils.py +0 -0
  165. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_key_bindings.py +0 -0
  166. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_lexer.py +0 -0
  167. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_batch.py +0 -0
  168. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  169. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_naive_completion.py +0 -0
  170. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_output.py +0 -0
  171. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_password_sources.py +0 -0
  172. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_polars_transform.py +0 -0
  173. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  174. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_history.py +0 -0
  175. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  176. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_schema_prefetcher.py +0 -0
  177. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_dbcommands.py +0 -0
  178. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_init.py +0 -0
  179. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_iocommands.py +0 -0
  180. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_llm.py +0 -0
  181. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_special_utils.py +0 -0
  182. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sql_utils.py +0 -0
  183. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlcompleter.py +0 -0
  184. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_sqlexecute.py +0 -0
  185. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_ssh_tunnel.py +0 -0
  186. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_string_utils.py +0 -0
  187. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_tabular_output.py +0 -0
  188. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_vault.py +0 -0
  189. {mycli-2.15.0 → mycli-2.16.0}/test/pytests/test_zsh_completion.py +0 -0
  190. {mycli-2.15.0 → mycli-2.16.0}/test/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.15.0
3
+ Version: 2.16.0
4
4
  Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
5
5
  Author: Mycli Core Team
6
6
  License-Expression: BSD-3-Clause
@@ -1,3 +1,20 @@
1
+ 2.16.0 (2026/08/22)
2
+ ==============
3
+
4
+ Features
5
+ ---------
6
+ * Advertise beta `--boundary-id` option in helpdoc and completions.
7
+ * Add `/source --special` to allow executing some special commands.
8
+ * Add `/source --show` to display each query before executing it.
9
+ * Add `/source --page` to display all output in one pager session.
10
+
11
+
12
+ Bug Fixes
13
+ ---------
14
+ * Show CLI error on invalid `--execute` containing `/source`.
15
+ * Require `/source` filenames containing spaces to be quoted.
16
+
17
+
1
18
  2.15.0 (2026/08/20)
2
19
  ==============
3
20
 
@@ -54,7 +54,7 @@ copy the previous query to the clipboard with /clip!
54
54
 
55
55
  edit a query in an external editor using <query>\edit!
56
56
 
57
- edit a query in an external editor using /edit <filename>!
57
+ edit a query in an external editor using /edit <file>!
58
58
 
59
59
  /f lists favorite queries; /f <name> executes a favorite!
60
60
 
@@ -68,7 +68,7 @@ Manage favorite queries with /favorite!
68
68
 
69
69
  /l lists databases!
70
70
 
71
- /once <filename> appends the next result to <filename>!
71
+ /once <file> appends the next result to <file>!
72
72
 
73
73
  /| <command> sends the next result to a subprocess!
74
74
 
@@ -306,9 +306,9 @@ customize password sources/precedence with "password_sources" in ~/.myclirc!
306
306
 
307
307
  redirect query output to a shell command with "$| <command>"!
308
308
 
309
- redirect query output to a CSV file with "$> <filename>"!
309
+ redirect query output to a CSV file with "$> <file>"!
310
310
 
311
- append query output to a CSV file with "$>> <filename>"!
311
+ append query output to a CSV file with "$>> <file>"!
312
312
 
313
313
  run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
314
314
 
@@ -4,16 +4,21 @@ from collections.abc import Generator, Mapping
4
4
  import logging
5
5
  import os
6
6
  import re
7
+ import shlex
7
8
  from typing import TYPE_CHECKING, Any, cast
8
9
 
9
10
  import click
11
+ import sqlparse
10
12
 
13
+ from mycli.compat import WIN
11
14
  from mycli.config import write_default_config
12
15
  from mycli.main_modes.repl import set_all_external_titles
13
16
  from mycli.packages import special
14
17
  from mycli.packages.batch_utils import statements_from_filehandle
15
18
  from mycli.packages.filepaths import dir_path_exists
16
19
  from mycli.packages.interactive_utils import confirm_destructive_query
20
+ from mycli.packages.special import main as special_main
21
+ from mycli.packages.special.iocommands import expand_favorite_query
17
22
  from mycli.packages.special.main import ArgType, SpecialCommandAlias
18
23
  from mycli.packages.sqlresult import SQLResult
19
24
  from mycli.sqlexecute import SQLExecute
@@ -30,6 +35,29 @@ MISSING_CONFIG_VALUE = object()
30
35
  DSN_CONFIG_VALUE = object()
31
36
  FAVORITES_CONFIG_VALUE = object()
32
37
  HIDDEN_CONFIG_SECTIONS = frozenset({'alias_dsn', 'favorite_queries'})
38
+ INVALID_SOURCE_FILENAME = 'Source accepts exactly one filename; filenames containing spaces must be quoted.'
39
+ SOURCE_SAFE_SPECIAL_COMMANDS = frozenset({
40
+ 'connect',
41
+ 'fd',
42
+ 'fs',
43
+ 'help',
44
+ 'l',
45
+ 'nowarnings',
46
+ 'prompt',
47
+ 'redirectformat',
48
+ 'rehash',
49
+ 'status',
50
+ 'tableformat',
51
+ 'timing',
52
+ 'use',
53
+ 'warnings',
54
+ 'dt',
55
+ })
56
+ SOURCE_SAFE_SUBCOMMANDS = {
57
+ 'config': frozenset({'help', 'get', 'search'}),
58
+ 'dsn': frozenset({'help', 'list', 'show', 'save', 'delete'}),
59
+ 'favorite': frozenset({'help', 'list', 'reload', 'run', 'save', 'delete'}),
60
+ }
33
61
 
34
62
 
35
63
  def _render_config_value(value: Any) -> str:
@@ -38,6 +66,106 @@ def _render_config_value(value: Any) -> str:
38
66
  return str(value)
39
67
 
40
68
 
69
+ def _parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
70
+ allow_special = False
71
+ show_queries = False
72
+ page_output = False
73
+ filename = arg
74
+ while arguments := filename.split(maxsplit=1):
75
+ if arguments[0] == '--special':
76
+ allow_special = True
77
+ elif arguments[0] == '--show':
78
+ show_queries = True
79
+ elif arguments[0] == '--page':
80
+ page_output = True
81
+ else:
82
+ break
83
+ filename = arguments[1] if len(arguments) == 2 else ''
84
+ return filename, allow_special, show_queries, page_output
85
+
86
+
87
+ def _has_unquoted_whitespace(value: str) -> bool:
88
+ quote: str | None = None
89
+ escaped = False
90
+ for character in value:
91
+ if escaped:
92
+ if quote is None and character.isspace():
93
+ return True
94
+ escaped = False
95
+ continue
96
+ if not WIN and character == '\\' and quote != "'":
97
+ escaped = True
98
+ continue
99
+ if character in ("'", '"'):
100
+ if quote is None:
101
+ quote = character
102
+ elif quote == character:
103
+ quote = None
104
+ elif quote is None and character.isspace():
105
+ return True
106
+ return False
107
+
108
+
109
+ def _parse_source_filename(filename: str) -> str:
110
+ if not filename:
111
+ return ''
112
+ if _has_unquoted_whitespace(filename):
113
+ raise ValueError(INVALID_SOURCE_FILENAME)
114
+ try:
115
+ arguments = shlex.split(filename, posix=not WIN)
116
+ except ValueError as error:
117
+ raise ValueError(f'Invalid source filename: {error}.') from None
118
+ if len(arguments) != 1:
119
+ raise ValueError(INVALID_SOURCE_FILENAME)
120
+ parsed_filename = arguments[0]
121
+ if WIN and len(parsed_filename) >= 2 and parsed_filename[0] == parsed_filename[-1] and parsed_filename[0] in ("'", '"'):
122
+ parsed_filename = parsed_filename[1:-1]
123
+ return parsed_filename
124
+
125
+
126
+ def _registered_special_command(query: str) -> tuple[str, str] | None:
127
+ command, _verbosity, arg = special.parse_special_command(query)
128
+ registered = special_main.COMMANDS.get(command)
129
+ if registered is None:
130
+ registered = special_main.COMMANDS.get(command.lower())
131
+ if registered is None:
132
+ return None
133
+ return registered.command.removeprefix('\\').removeprefix('/').lower(), arg
134
+
135
+
136
+ def _favorite_source_command_is_safe(arg: str) -> bool:
137
+ query, _error = expand_favorite_query(arg)
138
+ if query is None:
139
+ return True
140
+ return not any(special.is_special_command(statement.rstrip(';')) for statement in sqlparse.split(query))
141
+
142
+
143
+ def _source_special_command_is_safe(query: str) -> bool:
144
+ parsed = _registered_special_command(query)
145
+ if parsed is None:
146
+ return False
147
+
148
+ command, arg = parsed
149
+ if command == 'f':
150
+ return not arg or _favorite_source_command_is_safe(arg)
151
+ if command in ('fd', 'fs'):
152
+ return True
153
+ if command in SOURCE_SAFE_SPECIAL_COMMANDS:
154
+ return True
155
+
156
+ subcommands = SOURCE_SAFE_SUBCOMMANDS.get(command)
157
+ if subcommands is None:
158
+ return False
159
+ arguments = arg.split(maxsplit=1)
160
+ subcommand = arguments[0].lower() if arguments else 'help'
161
+ if subcommand not in subcommands:
162
+ return False
163
+ if command == 'favorite' and subcommand == 'run':
164
+ run_arg = arguments[1] if len(arguments) == 2 else ''
165
+ return not run_arg or _favorite_source_command_is_safe(run_arg)
166
+ return True
167
+
168
+
41
169
  def _iter_config_values(
42
170
  config: Mapping[str, Any],
43
171
  prefix: str = '',
@@ -143,7 +271,7 @@ class ClientCommandsMixin:
143
271
  special.register_special_command(
144
272
  self.execute_from_file,
145
273
  "source",
146
- "/source <filename>",
274
+ "/source [--special|--show|--page] <file>",
147
275
  "Execute queries from a file.",
148
276
  aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
149
277
  )
@@ -266,12 +394,20 @@ class ClientCommandsMixin:
266
394
  yield SQLResult(status=msg)
267
395
 
268
396
  def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
269
- if not arg:
270
- yield SQLResult(status="Missing required argument: filename.")
397
+ filename, allow_special, show_queries, page_output = _parse_source_arguments(arg)
398
+ if page_output:
399
+ yield SQLResult(command={'name': 'source_page'})
400
+ try:
401
+ filename = _parse_source_filename(filename)
402
+ except ValueError as error:
403
+ yield SQLResult(status=str(error), is_error=True)
404
+ return
405
+ if not filename:
406
+ yield SQLResult(status="Missing required argument: filename.", is_error=True)
271
407
  return
272
408
 
273
409
  try:
274
- file_h = open(os.path.expanduser(arg))
410
+ file_h = open(os.path.expanduser(filename))
275
411
  except OSError as error:
276
412
  yield SQLResult(status=str(error))
277
413
  return
@@ -288,12 +424,36 @@ class ClientCommandsMixin:
288
424
  yield SQLResult(status=str(error))
289
425
  return
290
426
 
291
- if special.is_special_command(query.rstrip(';')):
292
- yield SQLResult(status='Special commands are not supported in source files.')
293
- return
427
+ special_query = query.rstrip(';')
428
+ if special.is_special_command(special_query):
429
+ if not allow_special:
430
+ yield SQLResult(
431
+ status='Special commands are not supported without /source --special.',
432
+ is_error=True,
433
+ )
434
+ return
435
+ if not _source_special_command_is_safe(special_query):
436
+ command, _verbosity, _arg = special.parse_special_command(special_query)
437
+ yield SQLResult(
438
+ status=f'Special command is never permitted in source files: {command}.',
439
+ is_error=True,
440
+ )
441
+ return
442
+ if show_queries:
443
+ if page_output:
444
+ yield SQLResult(command={'name': 'source_show', 'text': special_query})
445
+ else:
446
+ click.secho(f'> {special_query}')
447
+ yield from self.sqlexecute.run(special_query)
448
+ continue
294
449
 
295
450
  if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
296
451
  continue
452
+ if show_queries:
453
+ if page_output:
454
+ yield SQLResult(command={'name': 'source_show', 'text': query})
455
+ else:
456
+ click.secho(f'> {query}')
297
457
  yield from self.sqlexecute.run(query)
298
458
 
299
459
  def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
@@ -10,6 +10,10 @@ from mycli.packages.sqlresult import SQLResult
10
10
  from mycli.sqlcompleter import SQLCompleter
11
11
 
12
12
 
13
+ class QueryError(Exception):
14
+ """An error reported while consuming query results."""
15
+
16
+
13
17
  class ClientQueryMixin:
14
18
  if TYPE_CHECKING:
15
19
  schema_prefetcher: Any
@@ -80,6 +84,7 @@ class ClientQueryMixin:
80
84
  query: str,
81
85
  checkpoint: str | None = None,
82
86
  new_line: bool = True,
87
+ raise_on_error: bool = False,
83
88
  ) -> None:
84
89
  """Runs *query*."""
85
90
  assert self.sqlexecute is not None
@@ -91,6 +96,10 @@ class ClientQueryMixin:
91
96
  self.main_formatter.query = query
92
97
  self.redirect_formatter.query = query
93
98
  self.explorer_formatter.query = query
99
+ if result.is_error and raise_on_error:
100
+ message = result.status_plain or 'Query failed.'
101
+ self.log_output(message)
102
+ raise QueryError(message)
94
103
  output = self.format_sqlresult(
95
104
  result,
96
105
  is_expanded=special.is_expanded_output(),
@@ -324,7 +324,6 @@ class CliArgs:
324
324
  boundary_id: str | None = clickdc.option(
325
325
  type=str,
326
326
  help='BETA: open a HashiCorp Boundary tunnel to TARGET_ID and connect through it.',
327
- hidden=True,
328
327
  )
329
328
  checkup: bool = clickdc.option(
330
329
  is_flag=True,
@@ -46,7 +46,7 @@ def main_execute_from_cli(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
46
46
  mycli.logger.warning('Unable to open TTY as stdin.')
47
47
  raise e
48
48
  if execution_confirmed:
49
- mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
49
+ mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint, raise_on_error=True)
50
50
  return 0
51
51
  else:
52
52
  return 1
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Iterable
3
+ from collections.abc import Generator, Iterable
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime
6
6
  import functools
7
7
  from functools import partial
8
8
  import html
9
9
  from importlib import resources
10
+ import itertools
10
11
  import os
11
12
  import random
12
13
  import re
@@ -441,6 +442,27 @@ def _output_results(
441
442
  sqlexecute = mycli.sqlexecute
442
443
  assert sqlexecute is not None
443
444
 
445
+ result_iterator = iter(results)
446
+ try:
447
+ first_result = next(result_iterator)
448
+ except StopIteration:
449
+ return
450
+
451
+ if first_result.command is not None and first_result.command['name'] == 'source_page':
452
+ if special.is_redirected():
453
+ _output_results(mycli, state, result_iterator, start)
454
+ return
455
+ paged_output = _single_paged_output_results(mycli, state, result_iterator, start)
456
+ try:
457
+ click.echo_via_pager(paged_output)
458
+ except KeyboardInterrupt:
459
+ pass
460
+ finally:
461
+ paged_output.close()
462
+ return
463
+
464
+ results = itertools.chain([first_result], result_iterator)
465
+
444
466
  result_count = 0
445
467
  watch_count = 0
446
468
  for result in results:
@@ -450,6 +472,9 @@ def _output_results(
450
472
  mycli.logger.debug('status: %r', result.status)
451
473
  mycli.logger.debug('command: %r', result.command)
452
474
  threshold = 1000
475
+ if result.command is not None and result.command['name'] == 'source_show':
476
+ click.secho(f"> {result.command['text']}")
477
+ continue
453
478
  if result.command is not None and result.command['name'] == 'set_buffer':
454
479
  state.buffer_text = str(result.command['text'])
455
480
  continue
@@ -534,6 +559,126 @@ def _output_results(
534
559
  mycli.output_timing(f'Time: {warnings_duration:0.03f}s', is_warnings_style=True)
535
560
 
536
561
 
562
+ def _single_paged_output_results(
563
+ mycli: 'MyCli',
564
+ state: ReplState,
565
+ results: Iterable[SQLResult],
566
+ start: float,
567
+ ) -> Generator[str, None, None]:
568
+ """Render results lazily through one pager session."""
569
+ sqlexecute = mycli.sqlexecute
570
+ assert sqlexecute is not None
571
+ result_iterator = iter(results)
572
+ result_count = 0
573
+ watch_count = 0
574
+ try:
575
+ for result in result_iterator:
576
+ mycli.logger.debug('preamble: %r', result.preamble)
577
+ mycli.logger.debug('header: %r', result.header)
578
+ mycli.logger.debug('rows: %r', result.rows)
579
+ mycli.logger.debug('status: %r', result.status)
580
+ mycli.logger.debug('command: %r', result.command)
581
+
582
+ if result.command is not None and result.command['name'] == 'source_show':
583
+ yield f"> {result.command['text']}\n"
584
+ continue
585
+ if result.command is not None and result.command['name'] == 'set_buffer':
586
+ state.buffer_text = str(result.command['text'])
587
+ continue
588
+ if result.command is not None and result.command['name'] == 'watch':
589
+ if watch_count > 0:
590
+ try:
591
+ start += float(result.command['seconds'])
592
+ except ValueError as error:
593
+ message = f'Invalid watch sleep time provided ({error}).'
594
+ mycli.log_output(message)
595
+ yield f'{message}\n'
596
+ return
597
+ else:
598
+ watch_count += 1
599
+
600
+ if mycli.auto_vertical_output:
601
+ if mycli.prompt_session is not None:
602
+ max_width = mycli.prompt_session.output.get_size().columns
603
+ else:
604
+ max_width = DEFAULT_WIDTH
605
+ else:
606
+ max_width = None
607
+
608
+ formatted = mycli.format_sqlresult(
609
+ result,
610
+ is_expanded=special.is_expanded_output(),
611
+ is_redirected=False,
612
+ null_string=mycli.null_string,
613
+ numeric_alignment=mycli.numeric_alignment,
614
+ binary_display=mycli.binary_display,
615
+ max_width=max_width,
616
+ )
617
+ duration = time.time() - start
618
+
619
+ if result_count > 0:
620
+ mycli.log_output('')
621
+ yield '\n'
622
+ for line in formatted:
623
+ mycli.log_output(line)
624
+ special.write_tee(line)
625
+ special.write_once(line)
626
+ special.write_pipe_once(line)
627
+ yield f'{line}\n'
628
+ if result.status:
629
+ mycli.log_output(result.status_plain)
630
+ yield f'{result.status_plain}\n'
631
+
632
+ if mycli.beep_after_seconds > 0 and duration >= mycli.beep_after_seconds:
633
+ assert mycli.prompt_session is not None
634
+ mycli.prompt_session.output.bell()
635
+ if special.is_timing_enabled():
636
+ timing = f'Time: {duration:0.03f}s'
637
+ mycli.log_output(timing)
638
+ yield f'{timing}\n'
639
+
640
+ start = time.time()
641
+ result_count += 1
642
+ state.mutating = state.mutating or is_mutating(result.status_plain)
643
+
644
+ if special.is_show_warnings_enabled() and isinstance(result.rows, Cursor) and result.rows.warning_count > 0:
645
+ warnings = sqlexecute.run('SHOW WARNINGS')
646
+ warnings_duration = time.time() - start
647
+ saw_warning = False
648
+ for warning in warnings:
649
+ saw_warning = True
650
+ warning_output = mycli.format_sqlresult(
651
+ warning,
652
+ is_expanded=special.is_expanded_output(),
653
+ is_redirected=False,
654
+ null_string=mycli.null_string,
655
+ numeric_alignment=mycli.numeric_alignment,
656
+ binary_display=mycli.binary_display,
657
+ max_width=max_width,
658
+ is_warnings_style=True,
659
+ )
660
+ mycli.log_output('')
661
+ yield '\n'
662
+ for line in warning_output:
663
+ mycli.log_output(line)
664
+ special.write_tee(line)
665
+ special.write_once(line)
666
+ special.write_pipe_once(line)
667
+ yield f'{line}\n'
668
+ if warning.status:
669
+ mycli.log_output(warning.status_plain)
670
+ yield f'{warning.status_plain}\n'
671
+
672
+ if saw_warning and special.is_timing_enabled():
673
+ timing = f'Time: {warnings_duration:0.03f}s'
674
+ mycli.log_output(timing)
675
+ yield f'{timing}\n'
676
+ finally:
677
+ close = getattr(result_iterator, 'close', None)
678
+ if close is not None:
679
+ close()
680
+
681
+
537
682
  def _keepalive_hook(
538
683
  mycli: 'MyCli',
539
684
  _context: Any,
@@ -703,6 +703,10 @@ def suggest_type(full_text: str, text_before_cursor: str) -> list[dict[str, Any]
703
703
  A scope for a column category will be a list of tables.
704
704
  """
705
705
 
706
+ stripped_text = text_before_cursor.lstrip()
707
+ if re.match(r'^(?:source|/source|\\\.|/\.)\s', stripped_text, re.IGNORECASE):
708
+ return suggest_special(text_before_cursor)
709
+
706
710
  word_before_cursor = last_word(text_before_cursor, include="many_punctuations")
707
711
 
708
712
  identifier: Identifier | None = None
@@ -811,6 +815,40 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
811
815
  r'/.',
812
816
  'source',
813
817
  '/source',
818
+ ]:
819
+ source_options = ['--special', '--show', '--page']
820
+ source_arguments = _arg.split()
821
+ if not source_arguments:
822
+ return [
823
+ {'type': 'special_subcommand', 'subcommands': source_options},
824
+ {'type': 'file_name', 'quote_spaces': True, 'source_filename': ''},
825
+ ]
826
+
827
+ used_options: set[str] = set()
828
+ argument_index = 0
829
+ while argument_index < len(source_arguments) and source_arguments[argument_index] in source_options:
830
+ used_options.add(source_arguments[argument_index])
831
+ argument_index += 1
832
+ remaining_options = [option for option in source_options if option not in used_options]
833
+ source_filename = _arg
834
+ for _index in range(argument_index):
835
+ parsed_argument = source_filename.split(maxsplit=1)
836
+ source_filename = parsed_argument[1] if len(parsed_argument) == 2 else ''
837
+ file_suggestion = {'type': 'file_name', 'quote_spaces': True, 'source_filename': source_filename}
838
+
839
+ if argument_index < len(source_arguments):
840
+ if source_arguments[argument_index].startswith('-'):
841
+ return [{'type': 'special_subcommand', 'subcommands': remaining_options}]
842
+ return [file_suggestion]
843
+ if not text[-1].isspace():
844
+ return []
845
+ suggestions: list[dict[str, Any]] = []
846
+ if remaining_options:
847
+ suggestions.append({'type': 'special_subcommand', 'subcommands': remaining_options})
848
+ suggestions.append(file_suggestion)
849
+ return suggestions
850
+
851
+ if cmd.lower() in [
814
852
  r'\o',
815
853
  '/o',
816
854
  r'\once',
@@ -85,7 +85,7 @@ def suggest_path(root_dir: str) -> list[str]:
85
85
  *list_path(os.curdir),
86
86
  ]
87
87
 
88
- if root_dir[0] not in ('/', '~') and root_dir[0:1] != './':
88
+ if root_dir[0] not in ('/', '~') and root_dir[0:2] != './':
89
89
  return list_path(os.curdir)
90
90
 
91
91
  if "~" in root_dir:
@@ -813,7 +813,7 @@ def parseargfile(arg: str) -> tuple[str, str]:
813
813
 
814
814
  @special_command(
815
815
  "tee",
816
- "/tee [-o] <filename>",
816
+ "/tee [-o] <file>",
817
817
  "Append all results to an output file (overwrite using -o).",
818
818
  )
819
819
  def set_tee(arg: str, **_) -> list[SQLResult]:
@@ -856,7 +856,7 @@ def write_tee(output: str | ANSI | FormattedText, nl: bool = True) -> None:
856
856
 
857
857
  @special_command(
858
858
  "\\once",
859
- "/once [-o] <filename>",
859
+ "/once [-o] <file>",
860
860
  "Append next result to an output file (overwrite using -o).",
861
861
  aliases=[SpecialCommandAlias("\\o", case_sensitive=False)],
862
862
  )
@@ -314,7 +314,7 @@ def quit_(*_args):
314
314
 
315
315
  @special_command(
316
316
  "\\edit",
317
- "/edit <filename> | <query>\\edit",
317
+ "/edit <file> | <query>\\edit",
318
318
  "Edit query with editor (uses $VISUAL or $EDITOR).",
319
319
  arg_type=ArgType.NO_ARGUMENT,
320
320
  case_sensitive=True,
@@ -17,11 +17,13 @@ class SQLResult:
17
17
  command: dict[str, str | float] | None = None
18
18
  image: bytes | None = None
19
19
  image_protocol: ImageProtocol = 'none'
20
+ is_error: bool = False
20
21
 
21
22
  def __str__(self):
22
23
  image = f'<{len(self.image)} bytes>' if self.image is not None else None
23
24
  return (
24
- f"{self.preamble}, {self.header}, {self.rows}, {self.postamble}, {self.status}, {self.command}, {image}, {self.image_protocol}"
25
+ f"{self.preamble}, {self.header}, {self.rows}, {self.postamble}, {self.status}, {self.command}, "
26
+ f"{image}, {self.image_protocol}, {self.is_error}"
25
27
  )
26
28
 
27
29
  @cached_property
@@ -37,7 +37,7 @@ _mycli_is_positional_db_arg() {
37
37
  ;;
38
38
  --*=*)
39
39
  ;;
40
- --host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options)
40
+ --host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options|--boundary-id)
41
41
  expects_value=1
42
42
  ;;
43
43
  -?*)
@@ -28,7 +28,7 @@ function _mycli_is_positional_db_arg
28
28
  case --
29
29
  set options_ended 1
30
30
  case '--*=*'
31
- case --host --hostname --port --user --username --socket --pass --password --password-file --vault-address --vault-mount --vault-secret --vault-password-field --vault-username-field --ssl-mode --ssl-ca --ssl-capath --ssl-cert --ssl-key --ssl-cipher --tls-version --database --dsn --completions --prompt --toolbar --logfile --checkpoint --myclirc --local-infile --login-path --execute --init-command --charset --character-set --batch --format --throttle --use-keyring --keepalive-ticks --ssh-jump --ssh-options
31
+ case --host --hostname --port --user --username --socket --pass --password --password-file --vault-address --vault-mount --vault-secret --vault-password-field --vault-username-field --ssl-mode --ssl-ca --ssl-capath --ssl-cert --ssl-key --ssl-cipher --tls-version --database --dsn --completions --prompt --toolbar --logfile --checkpoint --myclirc --local-infile --login-path --execute --init-command --charset --character-set --batch --format --throttle --use-keyring --keepalive-ticks --ssh-jump --ssh-options --boundary-id
32
32
  set expects_value 1
33
33
  case '-*'
34
34
  set -l option_length (string length -- "$word")
@@ -35,7 +35,7 @@ _mycli_is_positional_db_arg() {
35
35
  ;;
36
36
  --*=*)
37
37
  ;;
38
- --host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options)
38
+ --host|--hostname|--port|--user|--username|--socket|--pass|--password|--password-file|--vault-address|--vault-mount|--vault-secret|--vault-password-field|--vault-username-field|--ssl-mode|--ssl-ca|--ssl-capath|--ssl-cert|--ssl-key|--ssl-cipher|--tls-version|--database|--dsn|--completions|--prompt|--toolbar|--logfile|--checkpoint|--myclirc|--local-infile|--login-path|--execute|--init-command|--charset|--character-set|--batch|--format|--throttle|--use-keyring|--keepalive-ticks|--ssh-jump|--ssh-options|--boundary-id)
39
39
  expects_value=1
40
40
  ;;
41
41
  -?*)