Skip to content

Commit 382015f

Browse files
Merge pull request #115039 from ClickHouse/client-slash-command-hints
Offer the `/`-commands of the client, and name them when misspelled
2 parents 8377f22 + 4b34b6c commit 382015f

9 files changed

Lines changed: 500 additions & 24 deletions

File tree

docs/concepts/features/interfaces/client.mdx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,24 @@ help maxx_threads
174174

175175
Entering `help` on its own prints a short usage summary.
176176

177+
### Commands {#slash-commands}
178+
179+
The client runs a few `/`-prefixed commands itself instead of sending them to the server:
180+
181+
| Command | Description |
182+
|-------------------------------|--------------------------------------------------------------|
183+
| `/help <name>`, `/man <name>` | Show the documentation of `<name>`, the same as `help` above |
184+
| `/clear` | Clear the terminal |
185+
186+
Typing `/` at the beginning of the input lists the commands as [hints](#command-line-options-formatting), and typing more of a name narrows the list down; `Tab` completes the command being typed, which is also how the commands are completed when the hints are disabled. A `/` submitted on its own is not a command: as in Oracle SQL\*Plus, it repeats the last input. A misspelled command name is reported as such, with the commands it may have meant, instead of being run as a query:
187+
188+
```text
189+
:) /hepl
190+
191+
Exception on client:
192+
Code: 36. DB::Exception: Unknown command `/hepl`. Maybe you meant: ['/help']. Type `/` at the beginning of the line to see all the commands. (BAD_ARGUMENTS)
193+
```
194+
177195
### Query processing information {#processing-info}
178196

179197
When processing a query, the client shows:
@@ -878,7 +896,7 @@ See [Settings](/reference/settings/session-settings) for a list of settings.
878896
| `--echo-query-id [ <bool> ]` | Print the query id before execution. Takes an optional boolean value. | `true` in interactive mode, `false` in non-interactive (batch) mode |
879897
| `--echo-query-separator <string>` | Print this separator before the formatted echoed query (requires `--echo-formatted`), making it easier to tell the typed query apart from its reformatted echo. | Empty (disabled) |
880898
| `--highlight [ --hilite ] <bool>` | Toggle syntax highlighting of the command prompt and the echoed queries. | `true` |
881-
| `--hints <bool>` | Show as-you-type autocompletion hints (inline "ghost" text) for the best matching suggestion when the cursor is at the end of the input. Navigate the hints with Up/Down (or Ctrl-Up/Ctrl-Down); accept the inline hint with Tab or Right; `Enter` accepts a hint only after one has been explicitly selected and otherwise runs the query; `Tab` also opens the classic completion list. Requires `--highlight` (hints need color) and the suggestion machinery (so `--disable_suggestion` also turns them off). | `true` |
899+
| `--hints <bool>` | Show as-you-type autocompletion hints (inline "ghost" text) for the best matching suggestion when the cursor is at the end of the input. Navigate the hints with Up/Down (or Ctrl-Up/Ctrl-Down); accept the inline hint with Tab or Right; `Enter` accepts a hint only after one has been explicitly selected and otherwise runs the query; `Tab` also opens the classic completion list. Requires `--highlight` (hints need color). The suggestion hints also need the suggestion machinery, so `--disable_suggestion` turns those off; the [`/`-commands](#slash-commands) of the client are a static list and stay hinted as long as `--hints` and `--highlight` are on. `Tab` completes the `/`-commands even when the hints are off. | `true` |
882900

883901
### Execution details {#command-line-options-execution-details}
884902

docs/concepts/features/tools-and-utilities/clickhouse-local.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Arguments:
234234
- `--echo-query-id [ <bool> ]` — print the `query_id` before execution. Takes an optional boolean value. Enabled by default in interactive mode and disabled in batch mode.
235235
- `--echo-query-separator <string>` — print this separator before the formatted echoed query (requires `--echo-formatted`), making it easier to tell the typed query apart from its reformatted echo. Empty by default (disabled).
236236
- `--highlight`, `--hilite` `<bool>` — toggle syntax highlighting of the command prompt and the echoed queries. Enabled by default. Highlighting is applied only when writing to a terminal.
237-
- `--hints <bool>` — show as-you-type autocompletion hints (inline "ghost" text) for the best matching suggestion when the cursor is at the end of the input. Navigate the hints with Up/Down (or Ctrl-Up/Ctrl-Down); accept the inline hint with Tab or Right; `Enter` accepts a hint only after one has been explicitly selected and otherwise runs the query; `Tab` also opens the classic completion list. Requires `--highlight` (hints need color) and the suggestion machinery (so `--disable_suggestion` also turns them off). Enabled by default.
237+
- `--hints <bool>` — show as-you-type autocompletion hints (inline "ghost" text) for the best matching suggestion when the cursor is at the end of the input. Navigate the hints with Up/Down (or Ctrl-Up/Ctrl-Down); accept the inline hint with Tab or Right; `Enter` accepts a hint only after one has been explicitly selected and otherwise runs the query; `Tab` also opens the classic completion list. Requires `--highlight` (hints need color). The suggestion hints also need the suggestion machinery, so `--disable_suggestion` turns those off; the [`/`-commands](/interfaces/client#slash-commands) of the client are a static list and stay hinted as long as `--hints` and `--highlight` are on. `Tab` completes the `/`-commands even when the hints are off. Enabled by default.
238238
- `--verbose` — more details on query execution.
239239
- `--logger.console` — Log to console.
240240
- `--logger.log` — Log file name.

src/Client/ClientBase.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <Client/ClientBase.h>
55
#include <Client/ClientBaseHelpers.h>
6+
#include <Client/ClientSlashCommands.h>
67
#include <Client/InternalTextLogs.h>
78
#include <Client/LineReader.h>
89
#include <Client/TerminalKeystrokeInterceptor.h>
@@ -3686,6 +3687,8 @@ bool ClientBase::processQueryText(const String & text)
36863687
/// Clear the terminal (POSIX `clear`-style), not SQL. Same entry point as `ls` / `\i` meta-commands.
36873688
/// Only in interactive mode, or in clickhouse-local (including `-q`), so `clickhouse-client` batch
36883689
/// mode still parses `clear` as SQL and errors on mistakes (UNKNOWN_IDENTIFIER).
3690+
/// The `/`-form is also offered by the completion of the line editor - keep the `/`-commands
3691+
/// dispatched here in sync with `clientSlashCommands`.
36893692
if ((boost::iequals(trimmed_input, "clear") || boost::iequals(trimmed_input, "/clear"))
36903693
&& (is_interactive || supportsLocalMetaCommands()))
36913694
{
@@ -3758,6 +3761,8 @@ bool ClientBase::processQueryText(const String & text)
37583761
/// Interactive `help`/`man` command (in all of the forms `help`, `/help`, `man`, `/man`): render the
37593762
/// embedded documentation for a word from `system.documentation`. Gated like the other meta-commands,
37603763
/// so that batch `clickhouse-client` still parses a query starting with `help`/`man` as SQL.
3764+
/// The `/`-forms are also offered by the completion of the line editor - keep them in sync with
3765+
/// `clientSlashCommands`.
37613766
if (is_interactive || supportsLocalMetaCommands())
37623767
{
37633768
for (const std::string_view prefix : {"help", "/help", "man", "/man"})
@@ -3818,6 +3823,15 @@ bool ClientBase::processQueryText(const String & text)
38183823
}
38193824
#endif
38203825

3826+
/// A mistake in the name of a `/`-command would otherwise be parsed as SQL and reported as a
3827+
/// syntax error at the `/`, which tells the user nothing about the command they meant. Gated
3828+
/// like the commands themselves, so batch `clickhouse-client` still treats the input as SQL.
3829+
if (is_interactive || supportsLocalMetaCommands())
3830+
{
3831+
if (auto slash_command_error = diagnoseClientSlashCommand(trimmed_input))
3832+
throw Exception(ErrorCodes::BAD_ARGUMENTS, "{}", *slash_command_error);
3833+
}
3834+
38213835
if (query_fuzzer_runs)
38223836
{
38233837
processWithASTFuzzer(text);
@@ -4365,7 +4379,7 @@ void ClientBase::addCommonOptions(OptionsDescription & options_description)
43654379
("vertical,E", "Same as --format=Vertical or FORMAT Vertical or \\G at end of command")
43664380

43674381
("highlight,hilite", po::value<bool>()->default_value(true), "Toggle syntax highlighting of the command prompt and the echoed queries (can also use --hilite)")
4368-
("hints", po::value<bool>()->default_value(true), "Show as-you-type autocompletion hints (ghost text) in interactive mode; navigate with Up/Down or Ctrl-Up/Ctrl-Down. Accept the inline hint with Tab or Right; Enter accepts a hint only after one is explicitly selected, otherwise it runs the query. Requires --highlight and suggestions (disabled by --disable_suggestion). Disable with --hints 0.")
4382+
("hints", po::value<bool>()->default_value(true), "Show as-you-type autocompletion hints (ghost text) in interactive mode; navigate with Up/Down or Ctrl-Up/Ctrl-Down. Accept the inline hint with Tab or Right; Enter accepts a hint only after one is explicitly selected, otherwise it runs the query. Requires --highlight (hints need color). The suggestion hints also need the suggestions, so --disable_suggestion turns those off, while the client's /-commands are a static list and stay hinted. Disable with --hints 0.")
43694383

43704384
("ignore-error", "Do not stop processing after an error occurred")
43714385
("stacktrace", "Print stack traces of exceptions")
@@ -4806,12 +4820,14 @@ void ClientBase::runInteractive()
48064820
.ignore_shell_suspend = getClientConfiguration().getBool("ignore_shell_suspend", true),
48074821
.embedded_mode = isEmbeeddedClient(),
48084822
.interactive_history_legacy_keymap = getClientConfiguration().getBool("interactive_history_legacy_keymap", false),
4809-
/// Hints need color, so they are enabled only together with highlighting.
4810-
/// Hints need color (highlighting) and the suggestion machinery; `--disable_suggestion`
4811-
/// turns off autocompletion entirely, including the hints.
4823+
/// Hints need color, so they are enabled only together with highlighting. Client slash
4824+
/// commands have a static list and can therefore remain hinted when suggestions are off.
48124825
.enable_hints = ConfigHelper::getBool(getClientConfiguration(), "hints", true)
4813-
&& ConfigHelper::getBool(getClientConfiguration(), "highlight", true)
4814-
&& !getClientConfiguration().getBool("disable_suggestion", false),
4826+
&& ConfigHelper::getBool(getClientConfiguration(), "highlight", true),
4827+
.enable_suggestion_hints = !getClientConfiguration().getBool("disable_suggestion", false),
4828+
/// The `/`-commands (`/help`, `/man`, `/clear`) are the client's own; they are dispatched in
4829+
/// `processQueryText`, so offer them here.
4830+
.enable_slash_commands = true,
48154831
.extenders = query_extenders,
48164832
.delimiters = query_delimiters,
48174833
.word_break_characters = word_break_characters,

src/Client/ClientSlashCommands.cpp

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#include <Client/ClientSlashCommands.h>
2+
3+
#include <Common/NamePrompter.h>
4+
#include <Common/StringUtils.h>
5+
6+
#include <fmt/format.h>
7+
8+
#include <algorithm>
9+
10+
11+
namespace DB
12+
{
13+
14+
std::span<const ClientSlashCommand> clientSlashCommands()
15+
{
16+
/// Alphabetical, so that the order the commands are offered in is stable and predictable.
17+
static constexpr ClientSlashCommand commands[]
18+
{
19+
{"/clear", false},
20+
{"/help", true},
21+
{"/man", true},
22+
};
23+
return commands;
24+
}
25+
26+
namespace
27+
{
28+
29+
/// The commands are dispatched case-insensitively, so they are matched case-insensitively here as
30+
/// well (`/HELP` is a command, and it is completed too).
31+
bool startsWithCaseInsensitive(std::string_view s, std::string_view prefix)
32+
{
33+
return prefix.size() <= s.size() && equalsCaseInsensitive(s.substr(0, prefix.size()), prefix);
34+
}
35+
36+
/// How many commands to suggest for a misspelled name.
37+
constexpr size_t MAX_COMMAND_HINTS = 3;
38+
39+
/// The commands a misspelled name probably meant: the ones it is a prefix of (an unfinished name,
40+
/// which is the most likely mistake) and the ones within a typo distance of it.
41+
VectorWithMemoryTracking<String> getSimilarCommands(std::string_view name)
42+
{
43+
VectorWithMemoryTracking<String> all_names;
44+
for (const auto & command : clientSlashCommands())
45+
all_names.emplace_back(command.name);
46+
47+
/// `NamePrompter` skips candidates whose length differs too much from the name, so a short
48+
/// prefix (`/cl`) gets no hint from it - hence the prefix matches are collected separately.
49+
auto similar = NamePrompter<MAX_COMMAND_HINTS>::getHints(String(name), all_names);
50+
51+
VectorWithMemoryTracking<String> result;
52+
for (const auto & command_name : all_names)
53+
{
54+
const bool is_prefix = startsWithCaseInsensitive(command_name, name);
55+
if (is_prefix || std::find(similar.begin(), similar.end(), command_name) != similar.end())
56+
result.push_back(command_name);
57+
}
58+
return result;
59+
}
60+
61+
}
62+
63+
std::optional<String> diagnoseClientSlashCommand(std::string_view trimmed_input)
64+
{
65+
/// A `/` starts the command list, while `/*` opens a SQL comment.
66+
if (trimmed_input.empty() || trimmed_input[0] != '/'
67+
|| (trimmed_input.size() > 1 && !isAlphaASCII(trimmed_input[1])))
68+
return {};
69+
70+
const auto name_end = std::find_if(trimmed_input.begin(), trimmed_input.end(), [](char c) { return isWhitespaceASCII(c); });
71+
const size_t name_size = static_cast<size_t>(name_end - trimmed_input.begin());
72+
const std::string_view name = trimmed_input.substr(0, name_size);
73+
/// The input is trimmed, so whitespace after the name means that an argument follows it.
74+
const bool has_argument = name_size < trimmed_input.size();
75+
76+
for (const auto & command : clientSlashCommands())
77+
{
78+
if (!equalsCaseInsensitive(name, command.name))
79+
continue;
80+
if (!has_argument || command.takes_argument)
81+
return {}; /// A valid command, dispatched elsewhere.
82+
return fmt::format("The `{}` command does not accept an argument", command.name);
83+
}
84+
85+
String message = fmt::format("Unknown command `{}`", name);
86+
message += getHintsErrorMessageSuffix(getSimilarCommands(name));
87+
message += ". Type `/` at the beginning of the line to see all the commands";
88+
return message;
89+
}
90+
91+
ClientSlashCommandMatch matchClientSlashCommandPrefix(std::string_view text_before_cursor)
92+
{
93+
size_t pos = 0;
94+
/// Deliberately not skipping newlines: a `/` on a continuation line of a multiline query is a
95+
/// part of the query, not a command.
96+
while (pos < text_before_cursor.size() && isWhitespaceASCIIOneLine(text_before_cursor[pos]))
97+
++pos;
98+
99+
if (pos >= text_before_cursor.size() || text_before_cursor[pos] != '/')
100+
return {};
101+
102+
const std::string_view typed = text_before_cursor.substr(pos);
103+
104+
/// Only the name of the command is completed: what follows it is its argument.
105+
if (std::any_of(typed.begin(), typed.end(), [](char c) { return isWhitespaceASCII(c); }))
106+
return {};
107+
108+
ClientSlashCommandMatch match;
109+
for (const auto & command : clientSlashCommands())
110+
if (startsWithCaseInsensitive(command.name, typed))
111+
match.commands.emplace_back(command.name);
112+
113+
if (!match.commands.empty())
114+
match.prefix_length = typed.size();
115+
116+
return match;
117+
}
118+
119+
}

src/Client/ClientSlashCommands.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <optional>
4+
#include <span>
5+
#include <string>
6+
#include <string_view>
7+
#include <vector>
8+
9+
#include <base/types.h>
10+
11+
12+
namespace DB
13+
{
14+
15+
/// A `/`-prefixed meta-command of the client, such as `/help`. The client runs it itself instead of
16+
/// sending it to the server, and the line editor offers the commands as as-you-type hints and as Tab
17+
/// completions.
18+
struct ClientSlashCommand
19+
{
20+
std::string_view name;
21+
/// Whether an argument may follow the name, as in `/help MergeTree`.
22+
bool takes_argument;
23+
};
24+
25+
/// All the `/`-commands, in the order they are offered in.
26+
/// Keep in sync with the commands dispatched in `ClientBase::processQueryText`.
27+
std::span<const ClientSlashCommand> clientSlashCommands();
28+
29+
/// Diagnose input that looks like a `/`-command but is not one: a misspelled name (with the similar
30+
/// commands suggested) or an argument given to a command that takes none. Returns nothing when the
31+
/// input is a valid command, or when it is not a command at all (a `/* comment */`, ...) - it is then
32+
/// left to the SQL parser as before. The input has to be trimmed of whitespace and `;` already.
33+
std::optional<String> diagnoseClientSlashCommand(std::string_view trimmed_input);
34+
35+
/// The `/`-commands matching what is being typed; see `matchClientSlashCommandPrefix`.
36+
struct ClientSlashCommandMatch
37+
{
38+
/// The names of the matching commands; empty when the input is not a `/`-command prefix.
39+
std::vector<std::string> commands;
40+
/// How much of the input the commands replace: the `/` and everything typed after it. The
41+
/// commands are ASCII, so this is both a number of bytes and a number of code points (the line
42+
/// editor counts the context to replace in code points).
43+
size_t prefix_length = 0;
44+
};
45+
46+
/// Match the beginning of the input up to the cursor against the names of the `/`-commands, for the
47+
/// hints and the completion of the line editor. There is a match only while the command name itself
48+
/// is being typed - leading whitespace is skipped, but as soon as the name is followed by whitespace
49+
/// what is being typed is its argument, and a `/` that is not at the beginning of the input (in a
50+
/// `/* comment */`, on a continuation line, in a path) is not a command at all.
51+
ClientSlashCommandMatch matchClientSlashCommandPrefix(std::string_view text_before_cursor);
52+
53+
}

0 commit comments

Comments
 (0)