From 88f0208b9f638976bc81375ed158c9cc11d202d9 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 26 Jun 2026 06:45:02 -0400 Subject: [PATCH] advertise forward-slash /command forms in /help This is a quite limited approach, which meets the requirements by primarily tweaking the presentation only. Why take a limited approach? Because it implicitly keeps track of backward compatibility, for example, which commands can be entered without punctuation at all: "use mysql". In a followup it would be desirable to rewrite register_special_command() to take more parameters, such as "can be entered without punctuation", and a complete helpdoc to be returned on "/help /command". --- changelog.md | 1 + mycli/client_commands.py | 14 ++-- mycli/packages/special/dbcommands.py | 6 +- mycli/packages/special/iocommands.py | 30 ++++---- mycli/packages/special/main.py | 77 ++++++++++++-------- test/features/fixture_data/help_commands.txt | 76 +++++++++---------- test/pytests/test_special_main.py | 16 ++-- test/pytests/test_sqlexecute.py | 2 +- 8 files changed, 122 insertions(+), 100 deletions(-) diff --git a/changelog.md b/changelog.md index cde37bd6..aa840ede 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ Breaking Changes Features --------- * Add `--warn-batch` flag, which is off by default. +* Advertise forward-slash `/command` forms in `/help` output. Bug Fixes diff --git a/mycli/client_commands.py b/mycli/client_commands.py index a1588149..286acb98 100644 --- a/mycli/client_commands.py +++ b/mycli/client_commands.py @@ -35,14 +35,14 @@ def register_special_commands(self) -> None: special.register_special_command( self.change_db, "use", - "use ", + "/use ", "Change to a new database.", aliases=[SpecialCommandAlias("\\u", case_sensitive=False)], ) special.register_special_command( self.manual_reconnect, "connect", - "connect [database]", + "/connect [database]", "Reconnect to the server, optionally switching databases.", case_sensitive=True, aliases=[SpecialCommandAlias("\\r", case_sensitive=True)], @@ -50,7 +50,7 @@ def register_special_commands(self) -> None: special.register_special_command( self.refresh_completions, "rehash", - "rehash", + "/rehash", "Refresh auto-completions.", arg_type=ArgType.NO_QUERY, aliases=[SpecialCommandAlias("\\#", case_sensitive=False)], @@ -58,7 +58,7 @@ def register_special_commands(self) -> None: special.register_special_command( self.change_table_format, "tableformat", - "tableformat ", + "/tableformat ", "Change the table format used to output interactive results.", case_sensitive=True, aliases=[SpecialCommandAlias("\\T", case_sensitive=True)], @@ -66,7 +66,7 @@ def register_special_commands(self) -> None: special.register_special_command( self.change_redirect_format, "redirectformat", - "redirectformat ", + "/redirectformat ", "Change the table format used to output redirected results.", case_sensitive=True, aliases=[SpecialCommandAlias("\\Tr", case_sensitive=True)], @@ -74,14 +74,14 @@ def register_special_commands(self) -> None: special.register_special_command( self.execute_from_file, "source", - "source ", + "/source ", "Execute queries from a file.", aliases=[SpecialCommandAlias("\\.", case_sensitive=False)], ) special.register_special_command( self.change_prompt_format, "prompt", - "prompt ", + "/prompt ", "Change prompt format.", case_sensitive=True, aliases=[SpecialCommandAlias("\\R", case_sensitive=True)], diff --git a/mycli/packages/special/dbcommands.py b/mycli/packages/special/dbcommands.py index 0965efd3..592a3d47 100644 --- a/mycli/packages/special/dbcommands.py +++ b/mycli/packages/special/dbcommands.py @@ -22,7 +22,7 @@ @special_command( "\\dt", - "\\dt[+] [table]", + "/dt[+] [table]", "List or describe tables.", arg_type=ArgType.PARSED_QUERY, case_sensitive=True, @@ -61,7 +61,7 @@ def list_tables( @special_command( "\\l", - "\\l", + "/l", "List databases.", arg_type=ArgType.RAW_QUERY, case_sensitive=True, @@ -80,7 +80,7 @@ def list_databases(cur: Cursor, **_) -> list[SQLResult]: @special_command( "status", - "status", + "/status", "Get status information from the server.", arg_type=ArgType.RAW_QUERY, case_sensitive=True, diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index c8411279..3b7effd9 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -93,7 +93,7 @@ def is_show_warnings_enabled() -> bool: @special_command( 'warnings', - 'warnings', + '/warnings', 'Enable automatic warnings display.', arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -108,7 +108,7 @@ def enable_show_warnings() -> Generator[SQLResult, None, None]: @special_command( 'nowarnings', - 'nowarnings', + '/nowarnings', 'Disable automatic warnings display.', arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -123,7 +123,7 @@ def disable_show_warnings() -> Generator[SQLResult, None, None]: @special_command( "pager", - "pager [command]", + "/pager [command]", "Set pager to [command]. Print query results via pager.", arg_type=ArgType.PARSED_QUERY, case_sensitive=True, @@ -147,7 +147,7 @@ def set_pager(arg: str, **_) -> list[SQLResult]: @special_command( "nopager", - "nopager", + "/nopager", "Disable pager; print to stdout.", arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -160,7 +160,7 @@ def disable_pager() -> list[SQLResult]: @special_command( "\\timing", - "\\timing", + "/timing", "Toggle timing of queries.", arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -327,7 +327,7 @@ def set_redirect(command_part: str | None, file_operator_part: str | None, file_ @special_command( "\\f", - "\\f [name [args..]]", + "/f [name [args..]]", "List or execute favorite queries.", arg_type=ArgType.PARSED_QUERY, case_sensitive=True, @@ -403,7 +403,7 @@ def subst_favorite_query_args(query: str, args: list[str]) -> list[str | None]: @special_command( "\\fs", - "\\fs ", + "/fs ", "Save a favorite query.", ) def save_favorite_query(arg: str, **_) -> list[SQLResult]: @@ -425,7 +425,7 @@ def save_favorite_query(arg: str, **_) -> list[SQLResult]: @special_command( "\\fd", - "\\fd ", + "/fd ", "Delete a favorite query.", ) def delete_favorite_query(arg: str, **_) -> list[SQLResult]: @@ -441,7 +441,7 @@ def delete_favorite_query(arg: str, **_) -> list[SQLResult]: @special_command( "system", - "system [-r] ", + "/system [-r] ", "Execute a system shell command (raw mode with -r).", ) def execute_system_command(arg: str, **_) -> list[SQLResult]: @@ -522,7 +522,7 @@ def parseargfile(arg: str) -> tuple[str, str]: @special_command( "tee", - "tee [-o] ", + "/tee [-o] ", "Append all results to an output file (overwrite using -o).", ) def set_tee(arg: str, **_) -> list[SQLResult]: @@ -545,7 +545,7 @@ def close_tee() -> None: @special_command( "notee", - "notee", + "/notee", "Stop writing results to an output file.", ) def no_tee(arg: str, **_) -> list[SQLResult]: @@ -565,7 +565,7 @@ def write_tee(output: str | ANSI | FormattedText, nl: bool = True) -> None: @special_command( "\\once", - "\\once [-o] ", + "/once [-o] ", "Append next result to an output file (overwrite using -o).", aliases=[SpecialCommandAlias("\\o", case_sensitive=False)], ) @@ -623,7 +623,7 @@ def _run_post_redirect_hook(post_redirect_command: str, filename: str) -> None: @special_command( "\\pipe_once", - "\\pipe_once ", + "/pipe_once ", "Send next result to a subprocess.", aliases=[SpecialCommandAlias("\\|", case_sensitive=False)], ) @@ -687,7 +687,7 @@ def flush_pipe_once_if_written(post_redirect_command: str) -> None: @special_command( "watch", - "watch [seconds] [-c] ", + "/watch [seconds] [-c] ", "Execute query every [seconds] seconds (5 by default).", ) def watch_query(arg: str, **kwargs) -> Generator[SQLResult, None, None]: @@ -758,7 +758,7 @@ def watch_query(arg: str, **kwargs) -> Generator[SQLResult, None, None]: @special_command( "delimiter", - "delimiter ", + "/delimiter ", "Change end-of-statement delimiter.", ) def set_delimiter(arg: str, **_) -> list[SQLResult]: diff --git a/mycli/packages/special/main.py b/mycli/packages/special/main.py index 9463b46d..9f852b2a 100644 --- a/mycli/packages/special/main.py +++ b/mycli/packages/special/main.py @@ -48,6 +48,7 @@ class SpecialCommand: hidden: bool | None case_sensitive: bool | None aliases: list[SpecialCommandAlias] | None + backslash_only: bool class CommandNotFound(Exception): @@ -79,6 +80,7 @@ def special_command( hidden: bool = False, case_sensitive: bool = False, aliases: list[SpecialCommandAlias] | None = None, + backslash_only: bool = False, ) -> Callable: def wrapper(wrapped): register_special_command( @@ -90,6 +92,7 @@ def wrapper(wrapped): hidden=hidden, case_sensitive=case_sensitive, aliases=aliases, + backslash_only=backslash_only, ) return wrapped @@ -105,6 +108,7 @@ def register_special_command( hidden: bool = False, case_sensitive: bool = False, aliases: list[SpecialCommandAlias] | None = None, + backslash_only: bool = False, ) -> None: if command.startswith('\\'): forwardslash_command = '/' + command.removeprefix('\\') @@ -121,17 +125,20 @@ def register_special_command( hidden=hidden, case_sensitive=case_sensitive, aliases=aliases, + backslash_only=backslash_only, ) - COMMANDS[fcmd] = SpecialCommand( - handler, - command, - usage, - description, - arg_type=arg_type, - hidden=True, - case_sensitive=case_sensitive, - aliases=aliases, - ) + if not backslash_only: + COMMANDS[fcmd] = SpecialCommand( + handler, + command, + usage, + description, + arg_type=arg_type, + hidden=True, + case_sensitive=case_sensitive, + aliases=aliases, + backslash_only=backslash_only, + ) if case_sensitive: CASE_SENSITIVE_COMMANDS.add(command) CASE_SENSITIVE_COMMANDS.add(forwardslash_command) @@ -161,17 +168,20 @@ def register_special_command( case_sensitive=alias.case_sensitive, hidden=True, aliases=None, + backslash_only=backslash_only, ) - COMMANDS[fcmd] = SpecialCommand( - handler, - command, - usage, - description, - arg_type=arg_type, - case_sensitive=alias.case_sensitive, - hidden=True, - aliases=None, - ) + if not backslash_only: + COMMANDS[fcmd] = SpecialCommand( + handler, + command, + usage, + description, + arg_type=arg_type, + case_sensitive=alias.case_sensitive, + hidden=True, + aliases=None, + backslash_only=backslash_only, + ) def execute(cur: Cursor, sql: str) -> list[SQLResult]: @@ -207,7 +217,7 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]: @special_command( "help", - "help [term]", + "/help [term]", "Show this table, or search for help on a term.", arg_type=ArgType.NO_QUERY, aliases=[SpecialCommandAlias("\\?", case_sensitive=False), SpecialCommandAlias("?", case_sensitive=False)], @@ -221,10 +231,15 @@ def show_help(*_args) -> list[SQLResult]: continue if value.aliases: shortcut = value.aliases[0].command + if not value.backslash_only: + shortcut = '/' + shortcut.removeprefix('\\') else: shortcut = None - result.append((value.command, shortcut, value.usage, value.description)) - return [SQLResult(header=header, rows=result, postamble=f'Docs index — {DOCS_URL}')] + command = value.command + if not value.backslash_only: + command = '/' + command.removeprefix('\\') + result.append((command, shortcut, value.usage, value.description)) + return [SQLResult(header=header, rows=result, postamble=f'Use \\backslash forms when at end-of-line.\nDocs index — {DOCS_URL}')] def _show_special_help(keyword: str) -> list[SQLResult]: @@ -268,7 +283,7 @@ def show_keyword_help(cur: Cursor, arg: str) -> list[SQLResult]: return _show_mysql_help(cur, keyword) -@special_command('\\bug', '\\bug', 'File a bug on GitHub.', arg_type=ArgType.NO_QUERY) +@special_command('\\bug', '/bug', 'File a bug on GitHub.', arg_type=ArgType.NO_QUERY) def file_bug(*_args) -> list[SQLResult]: webbrowser.open_new_tab(ISSUES_URL) return [SQLResult(status=f'{ISSUES_URL} — press "New Issue"')] @@ -276,14 +291,14 @@ def file_bug(*_args) -> list[SQLResult]: @special_command( "exit", - "exit", + "/exit", "Exit.", arg_type=ArgType.NO_QUERY, aliases=[SpecialCommandAlias("\\q", case_sensitive=False)], ) @special_command( "quit", - "quit", + "/quit", "Quit.", arg_type=ArgType.NO_QUERY, aliases=[SpecialCommandAlias("\\q", case_sensitive=False)], @@ -294,7 +309,7 @@ def quit_(*_args): @special_command( "\\edit", - "\\edit | \\edit ", + "/edit | \\edit", "Edit query with editor (uses $VISUAL or $EDITOR).", arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -302,7 +317,7 @@ def quit_(*_args): ) @special_command( "\\clip", - "\\clip", + "/clip | \\clip", "Copy query to the system clipboard.", arg_type=ArgType.NO_QUERY, case_sensitive=True, @@ -313,6 +328,7 @@ def quit_(*_args): "Display query results vertically.", arg_type=ArgType.NO_QUERY, case_sensitive=True, + backslash_only=True, ) @special_command( "\\g", @@ -320,6 +336,7 @@ def quit_(*_args): "Display query results (mnemonic: go).", arg_type=ArgType.NO_QUERY, case_sensitive=True, + backslash_only=True, ) def stub(): raise NotImplementedError @@ -329,8 +346,8 @@ def stub(): @special_command( "\\llm", - "\\llm [arguments]", - "Interrogate an LLM. See \"\\llm help\".", + "/llm [arguments]", + "Interrogate an LLM. See \"/llm help\".", arg_type=ArgType.RAW_QUERY, case_sensitive=True, aliases=[SpecialCommandAlias("\\ai", case_sensitive=True)], diff --git a/test/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt index 26a23914..f3a25601 100644 --- a/test/features/fixture_data/help_commands.txt +++ b/test/features/fixture_data/help_commands.txt @@ -1,38 +1,38 @@ -+----------------+----------+---------------------------------+-------------------------------------------------------------+ -| Command | Shortcut | Usage | Description | -+----------------+----------+---------------------------------+-------------------------------------------------------------+ -| \bug | | \bug | File a bug on GitHub. | -| \clip | | \clip | Copy query to the system clipboard. | -| \dt | | \dt[+] [table] | List or describe tables. | -| \edit | \e | \edit | \edit | Edit query with editor (uses $VISUAL or $EDITOR). | -| \f | | \f [name [args..]] | List or execute favorite queries. | -| \fd | | \fd | Delete a favorite query. | -| \fs | | \fs | Save a favorite query. | -| \g | | \g | Display query results (mnemonic: go). | -| \G | | \G | Display query results vertically. | -| \l | | \l | List databases. | -| \llm | \ai | \llm [arguments] | Interrogate an LLM. See "\llm help". | -| \once | \o | \once [-o] | Append next result to an output file (overwrite using -o). | -| \pipe_once | \| | \pipe_once | Send next result to a subprocess. | -| \timing | \t | \timing | Toggle timing of queries. | -| connect | \r | connect [database] | Reconnect to the server, optionally switching databases. | -| delimiter | | delimiter | Change end-of-statement delimiter. | -| exit | \q | exit | Exit. | -| help | \? | help [term] | Show this table, or search for help on a term. | -| nopager | \n | nopager | Disable pager; print to stdout. | -| notee | | notee | Stop writing results to an output file. | -| nowarnings | \w | nowarnings | Disable automatic warnings display. | -| pager | \P | pager [command] | Set pager to [command]. Print query results via pager. | -| prompt | \R | prompt | Change prompt format. | -| quit | \q | quit | Quit. | -| redirectformat | \Tr | redirectformat | Change the table format used to output redirected results. | -| rehash | \# | rehash | Refresh auto-completions. | -| source | \. | source | Execute queries from a file. | -| status | \s | status | Get status information from the server. | -| system | | system [-r] | Execute a system shell command (raw mode with -r). | -| tableformat | \T | tableformat | Change the table format used to output interactive results. | -| tee | | tee [-o] | Append all results to an output file (overwrite using -o). | -| use | \u | use | Change to a new database. | -| warnings | \W | warnings | Enable automatic warnings display. | -| watch | | watch [seconds] [-c] | Execute query every [seconds] seconds (5 by default). | -+----------------+----------+---------------------------------+-------------------------------------------------------------+ ++-----------------+----------+---------------------------------+-------------------------------------------------------------+ +| Command | Shortcut | Usage | Description | ++-----------------+----------+---------------------------------+-------------------------------------------------------------+ +| /bug | | /bug | File a bug on GitHub. | +| /clip | | /clip | \clip | Copy query to the system clipboard. | +| /dt | | /dt[+] [table] | List or describe tables. | +| /edit | /e | /edit | \edit | Edit query with editor (uses $VISUAL or $EDITOR). | +| /f | | /f [name [args..]] | List or execute favorite queries. | +| /fd | | /fd | Delete a favorite query. | +| /fs | | /fs | Save a favorite query. | +| \g | | \g | Display query results (mnemonic: go). | +| \G | | \G | Display query results vertically. | +| /l | | /l | List databases. | +| /llm | /ai | /llm [arguments] | Interrogate an LLM. See "/llm help". | +| /once | /o | /once [-o] | Append next result to an output file (overwrite using -o). | +| /pipe_once | /| | /pipe_once | Send next result to a subprocess. | +| /timing | /t | /timing | Toggle timing of queries. | +| /connect | /r | /connect [database] | Reconnect to the server, optionally switching databases. | +| /delimiter | | /delimiter | Change end-of-statement delimiter. | +| /exit | /q | /exit | Exit. | +| /help | /? | /help [term] | Show this table, or search for help on a term. | +| /nopager | /n | /nopager | Disable pager; print to stdout. | +| /notee | | /notee | Stop writing results to an output file. | +| /nowarnings | /w | /nowarnings | Disable automatic warnings display. | +| /pager | /P | /pager [command] | Set pager to [command]. Print query results via pager. | +| /prompt | /R | /prompt | Change prompt format. | +| /quit | /q | /quit | Quit. | +| /redirectformat | /Tr | /redirectformat | Change the table format used to output redirected results. | +| /rehash | /# | /rehash | Refresh auto-completions. | +| /source | /. | /source | Execute queries from a file. | +| /status | /s | /status | Get status information from the server. | +| /system | | /system [-r] | Execute a system shell command (raw mode with -r). | +| /tableformat | /T | /tableformat | Change the table format used to output interactive results. | +| /tee | | /tee [-o] | Append all results to an output file (overwrite using -o). | +| /use | /u | /use | Change to a new database. | +| /warnings | /W | /warnings | Enable automatic warnings display. | +| /watch | | /watch [seconds] [-c] | Execute query every [seconds] seconds (5 by default). | ++-----------------+----------+---------------------------------+-------------------------------------------------------------+ diff --git a/test/pytests/test_special_main.py b/test/pytests/test_special_main.py index 6c2e620f..62f14f9a 100644 --- a/test/pytests/test_special_main.py +++ b/test/pytests/test_special_main.py @@ -93,6 +93,7 @@ def handler() -> None: hidden=False, case_sensitive=False, aliases=[special_main.SpecialCommandAlias('\\d', case_sensitive=False)], + backslash_only=False, ) assert special_main.COMMANDS['\\d'] == special_main.SpecialCommand( handler, @@ -103,6 +104,7 @@ def handler() -> None: hidden=True, case_sensitive=False, aliases=None, + backslash_only=False, ) @@ -181,6 +183,7 @@ def test_execute_raises_when_case_sensitive_exact_lookup_falls_back_to_lowercase hidden=False, case_sensitive=True, aliases=None, + backslash_only=False, ) special_main.CASE_SENSITIVE_COMMANDS.add('Camel') @@ -312,6 +315,7 @@ def test_execute_raises_for_unknown_arg_type(restore_commands: None) -> None: hidden=False, case_sensitive=False, aliases=None, + backslash_only=False, ) special_main.CASE_INSENSITIVE_COMMANDS.add('demo') @@ -324,7 +328,7 @@ def test_show_help_lists_only_visible_commands(restore_commands: None) -> None: special_main.register_special_command( lambda: None, 'visible', - 'visible', + '/visible', 'Visible command', aliases=[special_main.SpecialCommandAlias('\\v', case_sensitive=False)], ) @@ -333,8 +337,8 @@ def test_show_help_lists_only_visible_commands(restore_commands: None) -> None: result = special_main.show_help()[0] assert result.header == ['Command', 'Shortcut', 'Usage', 'Description'] - assert result.rows == [('visible', '\\v', 'visible', 'Visible command')] - assert result.postamble == f'Docs index — {DOCS_URL}' + assert result.rows == [('/visible', '/v', '/visible', 'Visible command')] + assert f'Docs index — {DOCS_URL}' in result.postamble def test_show_keyword_help_for_special_command(restore_commands: None) -> None: @@ -350,13 +354,13 @@ def test_show_keyword_help_for_special_command(restore_commands: None) -> None: def test_show_keyword_help_for_case_sensitive_special_alias() -> None: - result = special_main.show_keyword_help(cast(Any, None), r'\e')[0] + result = special_main.show_keyword_help(cast(Any, None), r'/e')[0] assert result.header == ['name', 'description', 'example'] assert result.rows == [ ( - r'\e', - '\\edit | \\edit \nEdit query with editor (uses $VISUAL or $EDITOR).', + r'/e', + '/edit | \\edit\nEdit query with editor (uses $VISUAL or $EDITOR).', '', ) ] diff --git a/test/pytests/test_sqlexecute.py b/test/pytests/test_sqlexecute.py index 660a8988..18b184f1 100644 --- a/test/pytests/test_sqlexecute.py +++ b/test/pytests/test_sqlexecute.py @@ -276,7 +276,7 @@ def test_collapsed_output_special_command(executor): @dbtest def test_special_command(executor): results = run(executor, "\\?") - assert_result_equal(results, rows=("quit", "\\q", "quit", "Quit."), header="Command", assert_contains=True, auto_status=False) + assert_result_equal(results, rows=("/quit", "/q", "/quit", "Quit."), header="Command", assert_contains=True, auto_status=False) @dbtest