Previously, virsh was able to complete initial command names,
as well as the names of flag options. However, this completion
was clunky for a number of reasons:
- it would provide a flag as an option for completion
even if it had already been used
- it did not support completion of positional arguments
- it would fall back to filename completion, even when
a file was not an appropriate argument
This commit improves virsh autocompletion by actually parsing
the line using the extracted parsing logic mentioned in the previous
commit. This allows for proper completion of positional arguments
and flag arguments, as well as preventing a flag from being passed
multiple times.
Additionally, it removes the default behavior of falling back to filename
completion when no matches are found. Now, the VSH_COMPLETE_AS_FILE
completer_flag may be passed with a null completer function to use
this functionality. Otherwise, no completion options are provided.
Note that this commit does not actually introduce any completers or
completer_flags; it just provides the base framework for using these.
---
tools/virsh.c | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tools/virsh.h | 5 ++
2 files changed, 167 insertions(+), 14 deletions(-)
diff --git a/tools/virsh.c b/tools/virsh.c
index 0273abe..2ac0875 100644
--- a/tools/virsh.c
+++ b/tools/virsh.c
@@ -3005,27 +3005,123 @@ vshReadlineCommandGenerator(const char *text, int state)
}
static char *
+vshDelegateToCustomCompleter(const vshCmdOptDef *opt,
+ const char *text, int state)
+{
+ static int list_index;
+ static char **completions = NULL;
+ int len = strlen(text);
+ char* val;
+
+ if (!state) {
+ if (!opt)
+ return NULL;
+
+ if (opt->completer) {
+ list_index = 0;
+ completions = opt->completer(opt->completer_flags);
+ }
+ /* otherwise, we fall back to the logic involving file
+ * completion below */
+ }
+
+ if (!completions) {
+ if (!opt->completer && opt->completer_flags &
VSH_COMPLETE_AS_FILE)
+ return rl_filename_completion_function(text, state);
+ else
+ return NULL;
+ }
+
+ while ((val = completions[list_index])) {
+ list_index++;
+
+ if (len && STRNEQLEN(val, text, len)) {
+ /* we need to free this explicitly
+ * since it's not getting sent
+ * to readline (frees values sent
+ * to it) */
+ VIR_FREE(val);
+ continue;
+ }
+
+ return val;
+ }
+
+ VIR_FREE(completions);
+ return NULL;
+}
+
+static char *
vshReadlineOptionsGenerator(const char *text, int state)
{
static int list_index, len;
static const vshCmdDef *cmd = NULL;
const char *name;
+ static int substate;
+ static uint32_t opts_seen = 0;
+ static uint32_t opts_need_arg = 0;
+ static const vshCmdOptDef *curr_opt = NULL;
+ static const vshCmdOptDef *last_arg_opt = NULL;
+ static bool waiting_for_flag_arg = false;
+ vshCommandParser parser;
if (!state) {
- /* determine command name */
- char *p;
- char *cmdname;
+ char *tok = NULL;
+ vshLineExtractionState line_state;
+ //bool waiting_for_next_command = false;
- if (!(p = strchr(rl_line_buffer, ' ')))
- return NULL;
+ len = strlen(text);
+ cmd = NULL;
+ opts_seen = 0;
+ opts_need_arg = 0;
+ curr_opt = NULL;
+ list_index = 0;
+ substate = 0;
+ waiting_for_flag_arg = false;
+
+ /* reset the parser */
+ memset(&parser, 0, sizeof(vshCommandParser));
+ parser.pos = rl_line_buffer;
+ parser.getNextArg = vshCommandStringGetArg;
+
+ line_state = vshExtractLinePart(NULL, &parser, &opts_seen,
+ &opts_need_arg, &tok, &cmd,
+ &curr_opt, false, 0);
+
+ while (line_state != VSH_LINE_STATE_LINE_DONE &&
+ line_state != VSH_LINE_STATE_TOK_ERR) {
+
+ /* if we have an opt and a tok, we're in a data
+ * arg or a flag with an arg */
+ if (line_state == VSH_LINE_STATE_IN_PROGRESS &&
+ curr_opt && tok) {
+ last_arg_opt = curr_opt;
+ } else {
+ last_arg_opt = NULL;
+ }
- cmdname = vshCalloc(NULL, (p - rl_line_buffer) + 1, 1);
- memcpy(cmdname, rl_line_buffer, p - rl_line_buffer);
+ if (line_state == VSH_LINE_STATE_CMD_DONE)
+ cmd = NULL;
- cmd = vshCmddefSearch(cmdname);
- list_index = 0;
- len = strlen(text);
- VIR_FREE(cmdname);
+ VIR_FREE(tok);
+ line_state = vshExtractLinePart(NULL, &parser, &opts_seen,
+ &opts_need_arg, &tok, &cmd,
+ &curr_opt, false, 1);
+ }
+
+ VIR_FREE(tok);
+
+ if (last_arg_opt && len > 0) {
+ if (last_arg_opt->type != VSH_OT_DATA &&
+ last_arg_opt->type != VSH_OT_ARGV)
+ curr_opt = last_arg_opt;
+ }
+
+ /* if we have an opt that wasn't reset, we're still waiting
+ * for a flag argument (ditto if we still have a last_arg_opt
+ * and we have current text) */
+ if (curr_opt)
+ waiting_for_flag_arg = true;
}
if (!cmd)
@@ -3034,14 +3130,61 @@ vshReadlineOptionsGenerator(const char *text, int state)
if (!cmd->opts)
return NULL;
+ if (waiting_for_flag_arg) {
+ char* res = vshDelegateToCustomCompleter(curr_opt, text, substate);
+ substate++;
+ /* if we're in a flag's argument, we don't
+ * want to show other flags */
+ return res;
+ }
+
while ((name = cmd->opts[list_index].name)) {
const vshCmdOptDef *opt = &cmd->opts[list_index];
+ const bool was_parsed = opts_seen & (1 << list_index);
char *res;
+ if (opt->type == VSH_OT_DATA || opt->type == VSH_OT_ARGV) {
+ bool in_completion = (was_parsed && last_arg_opt == opt &&
+ len > 0);
+ if (!in_completion && ffs(opts_need_arg) - 1 != list_index) {
+ list_index++;
+ continue;
+ }
+
+ /* skip positional args when we have the start of a flag */
+ if (len > 0 && text[0] == '-') {
+ list_index++;
+ continue;
+ }
+
+ /* we don't need to ignore args without custom completers,
+ * since vshDelegateToCustomCompleter will do this for us */
+ res = vshDelegateToCustomCompleter(opt, text, substate);
+ substate++;
+ if (res) {
+ return res;
+ } else {
+ /* if we're already in the middle of completing
+ * something with a custom completer, there's no
+ * need to show the other options */
+ if (len > 0 && text[0] != '-')
+ return NULL;
+ else {
+ list_index++;
+ continue;
+ }
+ }
+ }
+
+
list_index++;
- if (opt->type == VSH_OT_DATA || opt->type == VSH_OT_ARGV)
- /* ignore non --option */
+ /* ignore flags we've already parsed */
+ if (was_parsed)
+ continue;
+
+ /* skip if we've already started completing a data arg */
+ if (len && text[0] != '-')
continue;
if (len > 2) {
@@ -3069,6 +3212,10 @@ vshReadlineCompletion(const char *text, int start,
else
/* commands options */
matches = rl_completion_matches(text, vshReadlineOptionsGenerator);
+
+ /* tell the readline that we're ok with having no matches,
+ * so it shouldn't try to use its default completion function */
+ rl_attempted_completion_over = 1;
return matches;
}
@@ -3087,7 +3234,8 @@ vshReadlineInit(vshControl *ctl)
*/
rl_readline_name = (char *) "virsh";
- /* Tell the completer that we want a crack first. */
+ /* tell the completer that we want to handle generating
+ * potential matches */
rl_attempted_completion_function = vshReadlineCompletion;
/* Limit the total size of the history buffer */
diff --git a/tools/virsh.h b/tools/virsh.h
index 3e0251b..113d1e5 100644
--- a/tools/virsh.h
+++ b/tools/virsh.h
@@ -175,6 +175,11 @@ struct _vshCmdOptDef {
unsigned int completer_flags; /* option completer flags */
};
+/* a completer_flag, which, in the absence of a completer
+ * function, tells the completer to use the built-in
+ * readline file completer */
+# define VSH_COMPLETE_AS_FILE (1 << 8)
+
/*
* vshCmdOpt - command options
*
--
1.8.3.2