mycli 2.5.0__tar.gz → 2.6.1__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.
- {mycli-2.5.0/mycli.egg-info → mycli-2.6.1}/PKG-INFO +7 -4
- {mycli-2.5.0 → mycli-2.6.1}/README.md +4 -3
- {mycli-2.5.0 → mycli-2.6.1}/changelog.md +37 -0
- mycli-2.6.1/doc/screenshots/total_histogram.png +0 -0
- mycli-2.6.1/doc/transforms.md +128 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/TIPS +2 -2
- {mycli-2.5.0 → mycli-2.6.1}/mycli/app_state.py +11 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/cli_runner.py +4 -4
- {mycli-2.5.0 → mycli-2.6.1}/mycli/client.py +7 -0
- mycli-2.6.1/mycli/compat.py +27 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/repl.py +21 -6
- {mycli-2.5.0 → mycli-2.6.1}/mycli/myclirc +21 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/output.py +26 -1
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/completion_engine.py +12 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/hybrid_redirection.py +17 -4
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/polars_transform.py +144 -35
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/dsn_aliases.py +2 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/sqlresult.py +8 -1
- {mycli-2.5.0 → mycli-2.6.1}/mycli/sqlcompleter.py +22 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/types.py +6 -0
- {mycli-2.5.0 → mycli-2.6.1/mycli.egg-info}/PKG-INFO +7 -4
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/SOURCES.txt +1 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/requires.txt +2 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/scm_file_list.json +131 -130
- mycli-2.6.1/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.5.0 → mycli-2.6.1}/pyproject.toml +2 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/myclirc +62 -43
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_app_state.py +72 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client.py +70 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completion_engine.py +7 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_hybrid_redirection.py +50 -2
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_repl.py +123 -9
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_output.py +90 -1
- mycli-2.6.1/test/pytests/test_polars_transform.py +868 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_smart_completion_public_schema_only.py +30 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlcompleter.py +51 -0
- mycli-2.5.0/doc/transforms.md +0 -98
- mycli-2.5.0/mycli/compat.py +0 -5
- mycli-2.5.0/mycli.egg-info/scm_version.json +0 -8
- mycli-2.5.0/test/pytests/test_polars_transform.py +0 -405
- {mycli-2.5.0 → mycli-2.6.1}/.git-blame-ignore-revs +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/dependabot.yml +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/ci.yml +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/lint.yml +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/publish.yml +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/.gitignore +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/AGENTS.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/AUTHORS.rst +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/CONTRIBUTING.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/LICENSE.txt +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/MANIFEST.in +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/SPONSORS.rst +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/doc/key_bindings.rst +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/doc/llm.md +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/doc/screenshots/main.gif +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/doc/screenshots/tables.png +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/AUTHORS +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/SPONSORS +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/clibuffer.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/client_commands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/client_connection.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/client_query.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/clistyle.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/clitoolbar.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/completion_refresher.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/config.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/constants.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/key_bindings.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/lexer.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/batch.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/execute.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/filepaths.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/llm.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/main.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/string_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/password_sources.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/sqlexecute.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli/vault.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/setup.cfg +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/auto_vertical.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/basic_commands.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/connection.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/crud_database.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/crud_table.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/db_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/environment.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/fixture_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/iocommands.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/named_queries.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/specials.feature +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/__init__.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/connection.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/crud_database.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/crud_table.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/iocommands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/named_queries.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/specials.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/wrappers.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/features/wrappager.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/mylogin.cnf +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/conftest.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_checkup.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_query.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_config.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_lexer.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_init.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_main.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_vault.py +0 -0
- {mycli-2.5.0 → mycli-2.6.1}/test/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: CLI for MySQL Database. With auto-completion and syntax highlighting.
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -39,6 +39,7 @@ Requires-Dist: pip==26.*; extra == "llm"
|
|
|
39
39
|
Provides-Extra: dataframe
|
|
40
40
|
Requires-Dist: polars~=1.42.1; extra == "dataframe"
|
|
41
41
|
Requires-Dist: altair~=6.2.2; extra == "dataframe"
|
|
42
|
+
Requires-Dist: vl-convert-python~=1.9.0; extra == "dataframe"
|
|
42
43
|
Provides-Extra: all
|
|
43
44
|
Requires-Dist: mycli[dataframe,llm]; extra == "all"
|
|
44
45
|
Provides-Extra: dev
|
|
@@ -58,6 +59,7 @@ Requires-Dist: pip==26.*; extra == "dev"
|
|
|
58
59
|
Requires-Dist: ruff~=0.15.0; extra == "dev"
|
|
59
60
|
Requires-Dist: polars~=1.42.1; extra == "dev"
|
|
60
61
|
Requires-Dist: altair~=6.2.2; extra == "dev"
|
|
62
|
+
Requires-Dist: vl-convert-python~=1.9.0; extra == "dev"
|
|
61
63
|
Dynamic: license-file
|
|
62
64
|
|
|
63
65
|
# mycli
|
|
@@ -67,10 +69,11 @@ Dynamic: license-file
|
|
|
67
69
|
A command line client for MySQL with auto-completion and syntax highlighting.
|
|
68
70
|
|
|
69
71
|
Homepage: [https://mycli.net](https://mycli.net)
|
|
72
|
+
|
|
70
73
|
Documentation: [https://mycli.net/docs](https://mycli.net/docs)
|
|
71
74
|
|
|
72
|
-

|
|
73
|
-

|
|
75
|
+

|
|
76
|
+

|
|
74
77
|
|
|
75
78
|
Mycli is compatible with MySQL, MariaDB, Percona, TiDB, and Apache Doris.
|
|
76
79
|
|
|
@@ -133,7 +136,7 @@ Features
|
|
|
133
136
|
* Pretty print tabular data (with colors!).
|
|
134
137
|
* Support for SSL connections
|
|
135
138
|
* Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
|
|
136
|
-
* [Polars](https://pola.rs) dataframe [transforms](doc/transforms.md) with
|
|
139
|
+
* [Polars](https://pola.rs) dataframe [transforms and plots](doc/transforms.md) with `.|`, and Parquet saves with `.>`.
|
|
137
140
|
* Support for querying LLMs with context derived from your schema using `/llm`.
|
|
138
141
|
* Support for storing passwords in the system keyring.
|
|
139
142
|
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
A command line client for MySQL with auto-completion and syntax highlighting.
|
|
6
6
|
|
|
7
7
|
Homepage: [https://mycli.net](https://mycli.net)
|
|
8
|
+
|
|
8
9
|
Documentation: [https://mycli.net/docs](https://mycli.net/docs)
|
|
9
10
|
|
|
10
|
-

|
|
11
|
-

|
|
11
|
+

|
|
12
|
+

|
|
12
13
|
|
|
13
14
|
Mycli is compatible with MySQL, MariaDB, Percona, TiDB, and Apache Doris.
|
|
14
15
|
|
|
@@ -71,7 +72,7 @@ Features
|
|
|
71
72
|
* Pretty print tabular data (with colors!).
|
|
72
73
|
* Support for SSL connections
|
|
73
74
|
* Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
|
|
74
|
-
* [Polars](https://pola.rs) dataframe [transforms](doc/transforms.md) with
|
|
75
|
+
* [Polars](https://pola.rs) dataframe [transforms and plots](doc/transforms.md) with `.|`, and Parquet saves with `.>`.
|
|
75
76
|
* Support for querying LLMs with context derived from your schema using `/llm`.
|
|
76
77
|
* Support for storing passwords in the system keyring.
|
|
77
78
|
|
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
2.6.1 (2026/07/24)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
---------
|
|
6
|
+
* Subcommand completions for the `/dsn` command.
|
|
7
|
+
* Allow file target of `$>` redirection to be quoted.
|
|
8
|
+
* Display of inline plots returned from `.|` operations.
|
|
9
|
+
* Don't attempt inline plots in the Windows console.
|
|
10
|
+
* Save plots, like parquets, with the `.>` operator.
|
|
11
|
+
* Display many native Python types returned from `.|` operations.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Bug Fixes
|
|
15
|
+
---------
|
|
16
|
+
* Keep Vault username and password fields from being confused.
|
|
17
|
+
* Allow `\g` to terminate `.>` expressions.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Documentation
|
|
21
|
+
---------
|
|
22
|
+
* `README.md` formatting.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Internal
|
|
26
|
+
---------
|
|
27
|
+
* Sync test myclirc file closer to package myclirc file.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
2.5.1 (2026/07/22)
|
|
31
|
+
==============
|
|
32
|
+
|
|
33
|
+
Documentation
|
|
34
|
+
---------
|
|
35
|
+
* Attempt to make `README.md` images display on PyPi.
|
|
36
|
+
|
|
37
|
+
|
|
1
38
|
2.5.0 (2026/07/22)
|
|
2
39
|
==============
|
|
3
40
|
|
|
Binary file
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Transforms, Plots, and Parquets with Polars Dataframes
|
|
2
|
+
|
|
3
|
+
## Installing
|
|
4
|
+
|
|
5
|
+
Install mycli with dataframe support using:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install --upgrade 'mycli[dataframe]'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or install these libraries separately:
|
|
12
|
+
|
|
13
|
+
* [`polars`](https://pypi.org/project/polars/)
|
|
14
|
+
* [`altair`](https://pypi.org/project/altair/)
|
|
15
|
+
* [`vl-convert-python`](https://pypi.org/project/vl-convert-python/)
|
|
16
|
+
|
|
17
|
+
## BETA STATUS
|
|
18
|
+
|
|
19
|
+
Dataframe transforms and plots are new and experimental. The interface and
|
|
20
|
+
functionality may still change.
|
|
21
|
+
|
|
22
|
+
Here are some known limitations:
|
|
23
|
+
|
|
24
|
+
* transforms can't be composed with `$|` shell redirection
|
|
25
|
+
* composing multiple transform operations is not supported
|
|
26
|
+
* results from `UNION`s may be unable to be transformed
|
|
27
|
+
|
|
28
|
+
And there are inherent limitations to the post-processing model: the entire
|
|
29
|
+
SQL result must be transferred from the server and loaded into local memory.
|
|
30
|
+
|
|
31
|
+
## Transforming
|
|
32
|
+
|
|
33
|
+
In the interactive REPL, append the `.|` operator and a Python expression to a
|
|
34
|
+
single SQL statement. The Python expression receives
|
|
35
|
+
|
|
36
|
+
* `df`, a Polars `DataFrame`
|
|
37
|
+
* `pl`, the Polars module
|
|
38
|
+
* `alt`, the Altair module
|
|
39
|
+
|
|
40
|
+
Spaces may be required around the `.|` operator.
|
|
41
|
+
|
|
42
|
+
Transform example:
|
|
43
|
+
|
|
44
|
+
```sql
|
|
45
|
+
SELECT * FROM orders .| df.group_by('customer_id').len();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
which is equivalent to this native SQL:
|
|
49
|
+
|
|
50
|
+
```sql
|
|
51
|
+
SELECT customer_id, COUNT(1) AS len FROM orders GROUP BY customer_id;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Transform expressions run with normal Python privileges, and expressions
|
|
55
|
+
should not be run from untrusted sources.
|
|
56
|
+
|
|
57
|
+
If the transform operation returns a Polars `DataFrame` or `Series`, or a
|
|
58
|
+
Python type which can be rendered as a table, the result is rendered by mycli
|
|
59
|
+
as tabular output. `None` return values will be silently ignored, and other
|
|
60
|
+
return types will result in a warning message.
|
|
61
|
+
|
|
62
|
+
Transform expressions are useful for operations such as medians which
|
|
63
|
+
cannot be done (or are awkward) in SQL. Example:
|
|
64
|
+
|
|
65
|
+
```sql
|
|
66
|
+
SELECT * FROM orders .| df.describe();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Plotting
|
|
70
|
+
|
|
71
|
+
If the dataframe transform operation returns an Altair plot, the result can
|
|
72
|
+
be rendered as an inline PNG in many terminals.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
|
|
76
|
+
```sql
|
|
77
|
+
SELECT * FROM orders .| df['total'].plot.hist();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
<img src="https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/total_histogram.png" height=400>
|
|
81
|
+
|
|
82
|
+
Image size, display protocol, and other properties can be configured in
|
|
83
|
+
the `[dataframe]` section of `~/.myclirc`.
|
|
84
|
+
|
|
85
|
+
## Saving
|
|
86
|
+
|
|
87
|
+
A query result, transformed `DataFrame`, or transformed `Series` can be
|
|
88
|
+
written directly to a Parquet file with the `.>` operator. An Altair plot can
|
|
89
|
+
also be written to a file with the same operator.
|
|
90
|
+
|
|
91
|
+
Save example:
|
|
92
|
+
|
|
93
|
+
```sql
|
|
94
|
+
SELECT * FROM orders .> orders.parquet;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `.>` operator must be last, requires a `.parquet`, `.png`, `.pdf`, `.svg`,
|
|
98
|
+
or `.html` file extension on the destination, and overwrites any existing file.
|
|
99
|
+
|
|
100
|
+
Spaces may be required around the operator. Destination paths containing
|
|
101
|
+
whitespace must be quoted. A successful write reports its destination and row
|
|
102
|
+
count if appropriate.
|
|
103
|
+
|
|
104
|
+
When saving a plot, the format is deduced from the file extension.
|
|
105
|
+
|
|
106
|
+
When `post_redirect_command` is set in `~/.myclirc`, the given command runs
|
|
107
|
+
after a successful file save.
|
|
108
|
+
|
|
109
|
+
`.>` cannot be combined with the `\x` or `\G` special display terminators.
|
|
110
|
+
|
|
111
|
+
## Combining
|
|
112
|
+
|
|
113
|
+
Parquet saves may be combined with dataframe transforms. Again, the save
|
|
114
|
+
operator `.>` must be the last operator.
|
|
115
|
+
|
|
116
|
+
Combined transform and save examples:
|
|
117
|
+
|
|
118
|
+
```sql
|
|
119
|
+
SELECT * FROM orders .| df.group_by('customer_id').len() .> customer_counts.parquet;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```sql
|
|
123
|
+
SELECT * FROM orders .| df['order_id'] .> order_ids.parquet;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
SELECT * FROM orders .| df['total'].plot.hist() .> total_histogram.png;
|
|
128
|
+
```
|
|
@@ -296,8 +296,8 @@ customize password sources/precedence with "password_sources" in ~/.myclirc!
|
|
|
296
296
|
|
|
297
297
|
redirect query output to a shell command with "$| <command>"!
|
|
298
298
|
|
|
299
|
-
redirect query output to a file with "$> <filename>"!
|
|
299
|
+
redirect query output to a CSV file with "$> <filename>"!
|
|
300
300
|
|
|
301
|
-
append query output to a file with "$>> <filename>"!
|
|
301
|
+
append query output to a CSV file with "$>> <filename>"!
|
|
302
302
|
|
|
303
303
|
run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
from configobj import ConfigObj
|
|
7
7
|
|
|
8
8
|
from mycli.config import strip_matching_quotes
|
|
9
|
+
from mycli.types import ImageProtocol
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from mycli.client import MyCli
|
|
@@ -46,6 +47,16 @@ def normalize_ssl_mode(
|
|
|
46
47
|
return ssl_mode, error_notice
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
def normalize_image_protocol(image_protocol: str | None) -> tuple[ImageProtocol, str | None]:
|
|
51
|
+
if image_protocol == 'iterm2':
|
|
52
|
+
return 'iterm2', None
|
|
53
|
+
if image_protocol == 'kitty':
|
|
54
|
+
return 'kitty', None
|
|
55
|
+
if image_protocol in ('none', '', None):
|
|
56
|
+
return 'none', None
|
|
57
|
+
return 'none', f'Invalid config option provided for image_protocol ({image_protocol}); disabling.'
|
|
58
|
+
|
|
59
|
+
|
|
49
60
|
def configure_prompt_state(
|
|
50
61
|
mycli: MyCli,
|
|
51
62
|
config: ConfigObj,
|
|
@@ -384,13 +384,13 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
384
384
|
vault_config = mycli.config.get('vault_beta', {})
|
|
385
385
|
vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
|
|
386
386
|
vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
|
|
387
|
-
|
|
387
|
+
vault_password_field = cli_args.vault_password_field or vault_config.get('default_password_field') or DEFAULT_VAULT_PASSWORD_FIELD
|
|
388
388
|
vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
|
|
389
389
|
|
|
390
390
|
def load_vault_password() -> str | None:
|
|
391
391
|
try:
|
|
392
392
|
return get_field_from_vault(
|
|
393
|
-
|
|
393
|
+
vault_password_field,
|
|
394
394
|
vault_secret,
|
|
395
395
|
executable=vault_executable,
|
|
396
396
|
mount=vault_mount,
|
|
@@ -406,11 +406,11 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
406
406
|
vault_config = mycli.config.get('vault_beta', {})
|
|
407
407
|
vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
|
|
408
408
|
vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
|
|
409
|
-
|
|
409
|
+
vault_username_field = cli_args.vault_username_field or vault_config.get('default_username_field') or DEFAULT_VAULT_USERNAME_FIELD
|
|
410
410
|
vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
|
|
411
411
|
try:
|
|
412
412
|
vault_username = get_field_from_vault(
|
|
413
|
-
|
|
413
|
+
vault_username_field,
|
|
414
414
|
cli_args.vault_secret,
|
|
415
415
|
executable=vault_executable,
|
|
416
416
|
mount=vault_mount,
|
|
@@ -17,6 +17,7 @@ from mycli.app_state import (
|
|
|
17
17
|
configure_prompt_state,
|
|
18
18
|
destructive_keywords_from_config,
|
|
19
19
|
llm_prompt_truncation,
|
|
20
|
+
normalize_image_protocol,
|
|
20
21
|
normalize_ssl_mode,
|
|
21
22
|
)
|
|
22
23
|
from mycli.client_commands import ClientCommandsMixin
|
|
@@ -145,6 +146,12 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
145
146
|
self.null_string = c['main'].get('null_string')
|
|
146
147
|
self.numeric_alignment = c['main'].get('numeric_alignment', 'right') or 'right'
|
|
147
148
|
self.binary_display = c['main'].get('binary_display')
|
|
149
|
+
self.image_protocol, image_protocol_error = normalize_image_protocol(c['dataframe'].get('image_protocol'))
|
|
150
|
+
if image_protocol_error:
|
|
151
|
+
self.echo(image_protocol_error, err=True, fg='red')
|
|
152
|
+
self.plot_scale_factor = c['dataframe'].as_float('plot_scale_factor')
|
|
153
|
+
self.plot_ppi = c['dataframe'].as_int('plot_ppi')
|
|
154
|
+
self.plot_theme = c['dataframe'].get('plot_theme', 'carbong90') or 'carbong90'
|
|
148
155
|
self.llm_prompt_field_truncate, self.llm_prompt_section_truncate = llm_prompt_truncation(c)
|
|
149
156
|
|
|
150
157
|
self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c, self.config_without_package_defaults)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Platform and Python version compatibility support."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
WIN: bool = sys.platform in ("win32", "cygwin")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_win32() -> bool:
|
|
10
|
+
return sys.platform == 'win32'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_windows_console(output: object | None) -> bool:
|
|
14
|
+
"""Return whether output uses a native Windows console backend."""
|
|
15
|
+
if not _is_win32() or output is None:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
output_types = tuple(
|
|
19
|
+
getattr(import_module(module_name), class_name)
|
|
20
|
+
for module_name, class_name in (
|
|
21
|
+
('prompt_toolkit.output.conemu', 'ConEmuOutput'),
|
|
22
|
+
('prompt_toolkit.output.win32', 'Win32Output'),
|
|
23
|
+
('prompt_toolkit.output.windows10', 'Windows10_Output'),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return isinstance(output, output_types)
|
|
@@ -750,20 +750,35 @@ def _one_iteration(
|
|
|
750
750
|
mycli.explorer_formatter.query = text
|
|
751
751
|
if polars_transform is not None:
|
|
752
752
|
assert polars_pipeline is not None
|
|
753
|
-
if polars_pipeline.
|
|
754
|
-
polars_result = run_polars_transform(
|
|
753
|
+
if polars_pipeline.output_path is None:
|
|
754
|
+
polars_result = run_polars_transform(
|
|
755
|
+
polars_transform,
|
|
756
|
+
results,
|
|
757
|
+
image_protocol=mycli.image_protocol,
|
|
758
|
+
plot_scale_factor=mycli.plot_scale_factor,
|
|
759
|
+
plot_ppi=mycli.plot_ppi,
|
|
760
|
+
plot_theme=mycli.plot_theme,
|
|
761
|
+
)
|
|
755
762
|
else:
|
|
756
|
-
polars_result = run_polars_transform(
|
|
757
|
-
|
|
763
|
+
polars_result = run_polars_transform(
|
|
764
|
+
polars_transform,
|
|
765
|
+
results,
|
|
766
|
+
polars_pipeline.output_path,
|
|
767
|
+
image_protocol=mycli.image_protocol,
|
|
768
|
+
plot_scale_factor=mycli.plot_scale_factor,
|
|
769
|
+
plot_ppi=mycli.plot_ppi,
|
|
770
|
+
plot_theme=mycli.plot_theme,
|
|
771
|
+
)
|
|
772
|
+
if polars_pipeline.output_path is None:
|
|
758
773
|
if polars_pipeline.output_mode == 'explorer':
|
|
759
774
|
special.set_explorer_output(True)
|
|
760
775
|
elif polars_pipeline.output_mode == 'expanded':
|
|
761
776
|
special.set_expanded_output(True)
|
|
762
777
|
_output_results(mycli, state, iter([polars_result]), start)
|
|
763
|
-
if polars_pipeline.
|
|
778
|
+
if polars_pipeline.output_path is not None:
|
|
764
779
|
special.run_post_redirect_hook(
|
|
765
780
|
mycli.post_redirect_command,
|
|
766
|
-
polars_pipeline.
|
|
781
|
+
polars_pipeline.output_path,
|
|
767
782
|
)
|
|
768
783
|
else:
|
|
769
784
|
_output_results(mycli, state, results, start)
|
|
@@ -265,6 +265,27 @@ format = mysql_unicode
|
|
|
265
265
|
# Whether to remove the last line from the formatted output.
|
|
266
266
|
trim_footer = False
|
|
267
267
|
|
|
268
|
+
[dataframe]
|
|
269
|
+
|
|
270
|
+
# How to display inline image results in a terminal emulator. Possible values:
|
|
271
|
+
# * kitty
|
|
272
|
+
# * iterm2
|
|
273
|
+
# empty to disable
|
|
274
|
+
image_protocol = kitty
|
|
275
|
+
|
|
276
|
+
# PNG plot resolution in pixels per inch. Must be a positive integer.
|
|
277
|
+
# The apparent size is a combination of plot_ppi and plot_scale_factor.
|
|
278
|
+
plot_ppi = 200
|
|
279
|
+
|
|
280
|
+
# PNG plot resolution multiplier. Must be a positive number.
|
|
281
|
+
# The apparent size is a combination of plot_ppi and plot_scale_factor.
|
|
282
|
+
plot_scale_factor = 1.0
|
|
283
|
+
|
|
284
|
+
# Altair theme for rendering plots. Examples: carbong90, dark, default.
|
|
285
|
+
# Available themes depend on the installed libraries. See
|
|
286
|
+
# https://github.com/vega/vega-themes/#included-themes
|
|
287
|
+
plot_theme = carbong90
|
|
288
|
+
|
|
268
289
|
[search]
|
|
269
290
|
|
|
270
291
|
# Whether to apply syntax highlighting to the preview window in fuzzy history
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from decimal import Decimal
|
|
5
6
|
from io import TextIOWrapper
|
|
@@ -28,7 +29,7 @@ from prompt_toolkit.styles.style import _MergedStyle
|
|
|
28
29
|
from pygments.style import Style as PygmentsStyle
|
|
29
30
|
from pymysql.cursors import Cursor
|
|
30
31
|
|
|
31
|
-
from mycli.compat import WIN
|
|
32
|
+
from mycli.compat import WIN, is_windows_console
|
|
32
33
|
from mycli.constants import DEFAULT_HEIGHT, DEFAULT_WIDTH
|
|
33
34
|
import mycli.main_modes.repl as repl_mode
|
|
34
35
|
from mycli.packages import special
|
|
@@ -115,6 +116,14 @@ class OutputMixin(MyCliState):
|
|
|
115
116
|
is_warnings_style: bool = False,
|
|
116
117
|
) -> None:
|
|
117
118
|
"""Output text to stdout or a pager command."""
|
|
119
|
+
prompt_output = self.prompt_session.output if self.prompt_session is not None else None
|
|
120
|
+
if result.image is not None and not is_windows_console(prompt_output):
|
|
121
|
+
if result.image_protocol == 'iterm2':
|
|
122
|
+
click.secho('')
|
|
123
|
+
self.output_iterm2_image(result.image)
|
|
124
|
+
elif result.image_protocol == 'kitty':
|
|
125
|
+
click.secho('')
|
|
126
|
+
self.output_kitty_image(result.image)
|
|
118
127
|
if output:
|
|
119
128
|
if self.prompt_session is not None:
|
|
120
129
|
size = self.prompt_session.output.get_size()
|
|
@@ -186,6 +195,22 @@ class OutputMixin(MyCliState):
|
|
|
186
195
|
styled_status = to_formatted_text(status, style=add_style)
|
|
187
196
|
prompt_toolkit.print_formatted_text(styled_status, style=self.ptoolkit_style)
|
|
188
197
|
|
|
198
|
+
def output_iterm2_image(self, image: bytes) -> None:
|
|
199
|
+
"""Emit a PNG using the iTerm2 inline image protocol."""
|
|
200
|
+
filename = base64.b64encode(b'chart.png').decode('ascii')
|
|
201
|
+
contents = base64.b64encode(image).decode('ascii')
|
|
202
|
+
click.echo(f'\x1b]1337;File=name={filename};size={len(image)};width=auto;height=auto;preserveAspectRatio=1;inline=1:{contents}\x07')
|
|
203
|
+
|
|
204
|
+
def output_kitty_image(self, image: bytes) -> None:
|
|
205
|
+
"""Emit a PNG using Kitty's direct graphics protocol."""
|
|
206
|
+
contents = base64.b64encode(image).decode('ascii')
|
|
207
|
+
chunks = [contents[index : index + 4096] for index in range(0, len(contents), 4096)] or ['']
|
|
208
|
+
for index, chunk in enumerate(chunks):
|
|
209
|
+
action = 'a=T,f=100,' if index == 0 else ''
|
|
210
|
+
more = 1 if index < len(chunks) - 1 else 0
|
|
211
|
+
click.echo(f'\x1b_G{action}m={more};{chunk}\x1b\\', nl=False)
|
|
212
|
+
click.echo()
|
|
213
|
+
|
|
189
214
|
def configure_pager(self) -> None:
|
|
190
215
|
if not os.environ.get("LESS"):
|
|
191
216
|
os.environ["LESS"] = "-RXF"
|
|
@@ -6,6 +6,7 @@ from typing import Any, Callable, Literal
|
|
|
6
6
|
import sqlparse
|
|
7
7
|
from sqlparse.sql import Comparison, Identifier, Token, Where
|
|
8
8
|
|
|
9
|
+
from mycli.packages.special.dsn_aliases import DSN_SUBCOMMANDS
|
|
9
10
|
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
|
|
10
11
|
from mycli.packages.special.main import parse_special_command
|
|
11
12
|
from mycli.packages.sql_utils import extract_tables, find_prev_keyword, last_word
|
|
@@ -816,6 +817,17 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
|
|
|
816
817
|
if cmd in ["\\llm", "/llm", "\\ai", "/ai"]:
|
|
817
818
|
return [{"type": "llm"}]
|
|
818
819
|
|
|
820
|
+
if cmd.lower() in (r'\dsn', '/dsn'):
|
|
821
|
+
dsn_arguments = _arg.split(maxsplit=1)
|
|
822
|
+
completing_delete_target = (len(dsn_arguments) == 1 and text[-1].isspace()) or (len(dsn_arguments) == 2 and not text[-1].isspace())
|
|
823
|
+
if dsn_arguments and dsn_arguments[0].lower() == 'delete' and completing_delete_target:
|
|
824
|
+
return [{'type': 'dsn_alias'}]
|
|
825
|
+
if dsn_arguments and dsn_arguments[0].lower() == 'delete' and len(dsn_arguments) == 2:
|
|
826
|
+
return []
|
|
827
|
+
if dsn_arguments and dsn_arguments[0].lower() in DSN_SUBCOMMANDS - {'delete'}:
|
|
828
|
+
return []
|
|
829
|
+
return [{'type': 'special_subcommand', 'subcommands': list(DSN_SUBCOMMANDS)}]
|
|
830
|
+
|
|
819
831
|
return [{"type": "keyword"}, {"type": "special"}]
|
|
820
832
|
|
|
821
833
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import logging
|
|
3
|
+
import shlex
|
|
3
4
|
|
|
4
5
|
import sqlglot
|
|
5
6
|
|
|
@@ -113,9 +114,6 @@ def invalid_shell_part(
|
|
|
113
114
|
file_part: str | None,
|
|
114
115
|
command_part: str | None,
|
|
115
116
|
) -> bool:
|
|
116
|
-
if file_part and ' ' in file_part:
|
|
117
|
-
return True
|
|
118
|
-
|
|
119
117
|
if file_part and '>' in file_part:
|
|
120
118
|
return True
|
|
121
119
|
|
|
@@ -125,6 +123,19 @@ def invalid_shell_part(
|
|
|
125
123
|
return False
|
|
126
124
|
|
|
127
125
|
|
|
126
|
+
def parse_redirect_filename(file_part: str | None) -> str | None:
|
|
127
|
+
"""Return one unquoted redirect filename, or None for an invalid operand."""
|
|
128
|
+
if file_part is None or not file_part:
|
|
129
|
+
return None
|
|
130
|
+
if file_part[0] not in ('\'', '"'):
|
|
131
|
+
return file_part if not any(character.isspace() for character in file_part) else None
|
|
132
|
+
try:
|
|
133
|
+
parts = shlex.split(file_part)
|
|
134
|
+
except ValueError:
|
|
135
|
+
return None
|
|
136
|
+
return parts[0] if len(parts) == 1 else None
|
|
137
|
+
|
|
138
|
+
|
|
128
139
|
# todo there are still corner cases combining custom delimiters, caching, and redirection
|
|
129
140
|
@functools.lru_cache(maxsize=1)
|
|
130
141
|
def get_redirect_components(command: str) -> tuple[str | None, str | None, str | None, str | None]:
|
|
@@ -174,7 +185,9 @@ def get_redirect_components(command: str) -> tuple[str | None, str | None, str |
|
|
|
174
185
|
)
|
|
175
186
|
|
|
176
187
|
if file_part_tokens:
|
|
177
|
-
file_part = assemble_tokens(file_part_tokens)
|
|
188
|
+
file_part = parse_redirect_filename(assemble_tokens(file_part_tokens))
|
|
189
|
+
if file_part is None:
|
|
190
|
+
return None, None, None, None
|
|
178
191
|
else:
|
|
179
192
|
file_part = None
|
|
180
193
|
|