[PATCHv2 2/4] Improve virsh autocompletion (base framework)

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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.

This commit also introduces a helper macro called VSH_STRING_COMPLETER
to aid in the writing of completers which have a fixed set of strings.
A call of `VSH_STRING_COMPLETER(ctl, SomeOption, "string1", "string2")`
will generate a function `vshCompleteSomeOption`, suitable for use
as a custom completer.  The macro may accept up to 63 different strings
(a limitation of C VA_ARGS macros).  Additionally, definition of
vshStrndup (which calls virStrndup) was introduced, following the
vsh-prefixed memory-related functions.

Furthermore, two new commands have been introduced:

- 'complete' takes either a quoted partial line, or
  and unquoted partial line following a '--'.  It returns
  a newline-separated list of potential completions as expected
  by readline.  It is useful for writing commandline completers
  (bash completion, etc) and for testing.

- 'fake-command' takes a series of flags and data arguments, and
  prints those arguments which are present.  It is used to test
  the completion in cases not possible to cover with 'echo'.

Finally, a suite a tests have been introduced.
---
 tests/virshtest.c       | 255 ++++++++++++++++++++++++
 tools/Makefile.am       |   3 +-
 tools/virsh-completer.c |  49 +++++
 tools/virsh-completer.h |  73 +++++++
 tools/virsh.c           | 502 ++++++++++++++++++++++++++++++++++++++++++++++--
 tools/virsh.h           |  12 ++
 6 files changed, 878 insertions(+), 16 deletions(-)
 create mode 100644 tools/virsh-completer.c
 create mode 100644 tools/virsh-completer.h

diff --git a/tests/virshtest.c b/tests/virshtest.c
index 3fdae92..0c698fa 100644
--- a/tests/virshtest.c
+++ b/tests/virshtest.c
@@ -153,6 +153,189 @@ Memory size:         8192000 KiB\n\
   return testCompareOutputLit(exp, NULL, argv);
 }
 
+# if WITH_READLINE
+/* completion tests */
+
+static int testPartialCommandCompletionSing(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete", "li", NULL };
+    const char *exp = "list\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testPartialCommandCompletionMult(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete", "l", NULL };
+    const char *exp = "l\nlxc-enter-namespace\nlist\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testPartialBoolFlagCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "list --n", NULL };
+    const char *exp = "--n\n--no-autostart\n--name\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testBlankBoolFlagCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "list --", NULL };
+    const char *exp = "--\n--inactive\n--all\n--transient\n--persistent\n\
+--with-snapshot\n--without-snapshot\n--state-running\n\
+--state-paused\n--state-shutoff\n--state-other\n\
+--autostart\n--no-autostart\n--with-managed-save\n\
+--without-managed-save\n--uuid\n--name\n--table\n\
+--managed-save\n--title\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testNoCompleterDoesntFileComplete(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "attach-disk --serial ", NULL };
+    const char *exp = "\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testFileCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command --file ",
+                                 NULL };
+    char *actual = NULL;
+    int res = -1;
+
+    /* this may fail if you have a lot of files in the current
+     * directory.  4096 was not enough, but 4096^2 was on my system */
+    if (virtTestCaptureProgramOutput(argv, &actual, 4096*4096) < 0)
+        goto cleanup;
+
+    /* just check if we return something here */
+    if (actual && strlen(actual))
+        res = 0;
+
+ cleanup:
+    VIR_FREE(actual);
+    return res;
+}
+
+static int testUsedFlagIsntCompletedAgain(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command --string1 ab --str", NULL };
+    const char *exp = "--string\n--string2\n--string3\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testMultStrArgsCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command --string1 ", NULL };
+    const char *exp = "value\nvalue1\nvalue2\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testMultDataArgsCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command ab ", NULL };
+    const char *exp = "\n--abool\n\"i e f\"\n\"i g h\"\n--string1\n--string2\
+\n--string3\n--file\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testDataSpaceComplAreQuoted(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command ab ", NULL };
+    const char *exp = "\n--abool\n\"i e f\"\n\"i g h\"\n--string1\n--string2\
+\n--string3\n--file\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testStrSpaceComplAreQuoted(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command --string2 ", NULL };
+    const char *exp = "\"value \n\"value a\"\n\"value b\"\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+/* Note: partial completion is a bit funky because readline treats spaces as
+ * "new token", so we only need to complete part of the word.  Also, it ignores
+ * quotes, so if we already have a quote at the beginning of a token, our
+ * completion shouldn't have a quote */
+static int testPartialStrQuoteCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    int res = 0;
+    const char *const argv1[] = { VIRSH_CUSTOM, "complete",
+                                  "fake-command --string2 \"value ", NULL };
+    const char *exp1 = "\na\"\nb\"\n\n";
+    const char *const argv2[] = { VIRSH_CUSTOM, "complete",
+                                 "fake-command --string2 \"va", NULL };
+    const char *exp2 = "value \nvalue a\"\nvalue b\"\n\n";
+
+    res = testCompareOutputLit(exp1, NULL, argv1);
+    res += testCompareOutputLit(exp2, NULL, argv2);
+
+    return res;
+}
+
+static int testPartialDataQuoteCompletion(const void *data ATTRIBUTE_UNUSED)
+{
+    int res = 0;
+    const char *const argv1[] = { VIRSH_CUSTOM, "complete",
+                                  "fake-command ab \"i ", NULL };
+    const char *exp1 = "\ne f\"\ng h\"\n\n";
+    const char *const argv2[] = { VIRSH_CUSTOM, "complete",
+                                  "fake-command ab \"i e", NULL };
+    const char *exp2 = "e f\"\n\n";
+
+    res = testCompareOutputLit(exp1, NULL, argv1);
+    res += testCompareOutputLit(exp2, NULL, argv2);
+
+    return res;
+}
+
+static int testArgvCompletesRepeatedly(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "echo hi ", NULL };
+
+    const char *exp = "\n--shell\n--xml\n--str\n--hi\n\
+hello\nbonjour\nshalom\n#!\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testRepeatedCompletionRequests(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM,
+                                 "complete 'l'; complete 'li'", NULL };
+    const char *exp = "l\nlxc-enter-namespace\nlist\n\nlist\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testCompletionIgnoresSubcmd(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "list --all; echo h", NULL };
+    const char *exp = "hello\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+static int testCompletionInArgvMode(const void *data ATTRIBUTE_UNUSED)
+{
+    const char *const argv[] = { VIRSH_CUSTOM, "complete",
+                                 "--", "echo", "--shell", "a", "", NULL };
+    const char *exp = "\n--xml\n--str\n--hi\nhello\nbonjour\nshalom\n#!\n\n";
+    return testCompareOutputLit(exp, NULL, argv);
+}
+
+# endif /* WITH_READLINE */
+
+
 static int testCompareDominfoByID(const void *data ATTRIBUTE_UNUSED)
 {
   const char *const argv[] = { VIRSH_CUSTOM, "dominfo", "2", NULL };
@@ -322,6 +505,78 @@ mymain(void)
                     testCompareDomstateByName, NULL) != 0)
         ret = -1;
 
+# if WITH_READLINE
+    /* test completion */
+    if (virtTestRun("virsh completion (command with only one result)",
+                    testPartialCommandCompletionSing, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (command with multiple results)",
+                    testPartialCommandCompletionMult, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (boolean flag with parital name)",
+                    testPartialBoolFlagCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (boolean flag with only --)",
+                    testBlankBoolFlagCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (null completer)",
+                    testNoCompleterDoesntFileComplete, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (file completion)",
+                    testFileCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (no reusing flags)",
+                    testUsedFlagIsntCompletedAgain, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (multiple string flags)",
+                    testMultStrArgsCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (multiple data flags)",
+                    testMultDataArgsCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (repeated argv)",
+                    testArgvCompletesRepeatedly, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (spaces in data completions)",
+                    testDataSpaceComplAreQuoted, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (spaces in flag completions)",
+                    testStrSpaceComplAreQuoted, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (partial quoted data)",
+                    testPartialDataQuoteCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (partial quoted flag args)",
+                    testPartialStrQuoteCompletion, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (repeated completion requests)",
+                    testRepeatedCompletionRequests, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (completion ignores previous subcmd)",
+                    testCompletionIgnoresSubcmd, NULL) != 0)
+        ret = -1;
+
+    if (virtTestRun("virsh completion (completion command in argv mode)",
+                    testCompletionInArgvMode, NULL) != 0)
+        ret = -1;
+
+# endif /* WITH_READLINE */
+
     /* It's a bit awkward listing result before argument, but that's a
      * limitation of C99 vararg macros.  */
 # define DO_TEST(i, result, ...)                                        \
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 6847f13..e660720 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -53,7 +53,7 @@ EXTRA_DIST = \
 	virsh-network.c virsh-nodedev.c			\
 	virsh-nwfilter.c virsh-pool.c			\
 	virsh-secret.c virsh-snapshot.c			\
-	virsh-volume.c
+	virsh-volume.c virsh-completer.c
 
 
 
@@ -191,6 +191,7 @@ virsh_SOURCES =							\
 		virsh-secret.c virsh-secret.h			\
 		virsh-snapshot.c virsh-snapshot.h		\
 		virsh-volume.c virsh-volume.h			\
+		virsh-completer.c virsh-completer.h		\
 		$(NULL)
 
 virsh_LDFLAGS = \
diff --git a/tools/virsh-completer.c b/tools/virsh-completer.c
new file mode 100644
index 0000000..3e6c8f8
--- /dev/null
+++ b/tools/virsh-completer.c
@@ -0,0 +1,49 @@
+/*
+ * virsh-completer.c: Common custom completion utilities
+ *
+ * Copyright (C) 2005, 2007-2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include "virsh-completer.h"
+
+#include <stdarg.h>
+
+#include "conf/domain_conf.h"
+#include "viralloc.h"
+
+/* Utils - General */
+char **
+vshVarArgsToStringList(vshControl *ctl, unsigned int count, ...)
+{
+    va_list ap;
+    char **strs;
+    size_t i;
+
+    if (count == 0)
+        return NULL;
+
+    va_start(ap, count);
+
+    strs = vshCalloc(ctl, count, sizeof(char*));
+    for (i = 0; i < count; i++)
+        strs[i] = vshStrdup(ctl, va_arg(ap, char*));
+
+    va_end(ap);
+
+    return strs;
+};
diff --git a/tools/virsh-completer.h b/tools/virsh-completer.h
new file mode 100644
index 0000000..a565c5d
--- /dev/null
+++ b/tools/virsh-completer.h
@@ -0,0 +1,73 @@
+/*
+ * virsh-completer.h: Common custom completion utilities
+ *
+ * Copyright (C) 2005, 2007-2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef VIRSH_COMPLETER_H
+# define VIRSH_COMPLETER_H
+
+# include "virsh.h"
+
+/* Utils - General */
+/* __VA_NARGS__:
+ *
+ * This macro determine the length (up to 63) of
+ * __VA_ARGS__ arguments passed to a macro.
+ */
+
+/* inspired by
+ * https://groups.google.com/forum/#!topic/comp.std.c/d-6Mj5Lko_s */
+# define __VA_NARGS__(...) \
+    __VA_NARGS_FLATTEN__(__VA_ARGS__,INV_NUM_SEQ())
+# define __VA_NARGS_FLATTEN__(...) \
+    __VA_NARGS_IMPL__(__VA_ARGS__)
+# define __VA_NARGS_IMPL__( \
+    _1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15,_16, \
+    _17,_18,_19,_20,_21,_22,_23,_24,_25,_26,_27,_28,_29,_30, \
+    _31,_32,_33,_34,_35,_36,_37,_38,_39,_40,_41,_42,_43,_44, \
+    _45,_46,_47,_48,_49,_50,_51,_52,_53,_54,_55,_56,_57,_58, \
+    _59,_60,_61,_62,_63, N, ...) N
+# define INV_NUM_SEQ() \
+    63,62,61,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46, \
+    45,44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29,28, \
+    27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10, \
+    9,8,7,6,5,4,3,2,1,0
+
+/* VSH_STRING_COMPLETER:
+ *
+ * @ctl: a vshControl* or NULL
+ * @name: the name of the completer (unquoted)
+ * @__VA_ARGS__: the options as strings
+ *
+ * This macro creates a vshComplete[name] function
+ * suitable to for use as a custom option completer.
+ * The completer will return an array of strings with
+ * the values specified.
+ */
+# define VSH_STRING_COMPLETER(ctl, name, ...) \
+    static char ** \
+    vshComplete ## name (unsigned int flags) \
+    { \
+        virCheckFlags(0, NULL); \
+        return vshVarArgsToStringList(ctl, __VA_NARGS__(__VA_ARGS__), \
+                                      __VA_ARGS__); \
+    }
+
+char ** vshVarArgsToStringList(vshControl *ctl, unsigned int count, ...);
+
+#endif /* VIRSH_COMPLETER_H */
diff --git a/tools/virsh.c b/tools/virsh.c
index 4f87c20..808a125 100644
--- a/tools/virsh.c
+++ b/tools/virsh.c
@@ -85,6 +85,7 @@
 #include "virsh-secret.h"
 #include "virsh-snapshot.h"
 #include "virsh-volume.h"
+#include "virsh-completer.h"
 
 /* Gnulib doesn't guarantee SA_SIGINFO support.  */
 #ifndef SA_SIGINFO
@@ -139,6 +140,19 @@ _vshStrdup(vshControl *ctl, const char *s, const char *filename, int line)
 /* Poison the raw allocating identifiers in favor of our vsh variants.  */
 #define strdup use_vshStrdup_instead_of_strdup
 
+char *_vshStrncpy(vshControl *ctl, char *dest, const char *src, size_t n,
+                  size_t destsize, const char *filename, int line)
+{
+    char *res = virStrncpy(dest, src, n, destsize);
+
+    if (res)
+        return res;
+
+    vshError(ctl, _("%s: %d: failed to strncpy %zu characters"),
+             filename, line, n);
+    exit(EXIT_FAILURE);
+}
+
 int
 vshNameSorter(const void *a, const void *b)
 {
@@ -916,6 +930,9 @@ static const vshCmdInfo info_echo[] = {
     {.name = NULL}
 };
 
+/* say hello in English, French, Hebrew, and Shell ;-) */
+VSH_STRING_COMPLETER(NULL, EchoString, "hello", "bonjour", "shalom", "#!");
+
 static const vshCmdOptDef opts_echo[] = {
     {.name = "shell",
      .type = VSH_OT_BOOL,
@@ -935,6 +952,7 @@ static const vshCmdOptDef opts_echo[] = {
     },
     {.name = "string",
      .type = VSH_OT_ARGV,
+     .completer = vshCompleteEchoString,
      .help = N_("arguments to echo")
     },
     {.name = NULL}
@@ -998,6 +1016,80 @@ cmdEcho(vshControl *ctl, const vshCmd *cmd)
 }
 
 /*
+ * "fake-command" command
+ */
+static const vshCmdInfo info_fake_command[] = {
+    {.name = "help",
+     .data = N_("a fake, no-op command")
+    },
+    {.name = "desc",
+     .data = N_("Do absolutely nothing! Used for testing completion")
+    },
+    {.name = NULL}
+};
+
+VSH_STRING_COMPLETER(NULL, FakeCommandStr1, "value1", "value2");
+VSH_STRING_COMPLETER(NULL, FakeCommandStr2, "value a", "value b");
+VSH_STRING_COMPLETER(NULL, FakeCommandData1, "ab", "cd");
+VSH_STRING_COMPLETER(NULL, FakeCommandData2, "i e f", "i g h");
+
+static const vshCmdOptDef opts_fake_command[] = {
+    {.name = "abool",
+     .type = VSH_OT_BOOL,
+     .help = N_("a boolean flag")
+    },
+    {.name = "data1",
+     .type = VSH_OT_DATA,
+     .completer = vshCompleteFakeCommandData1,
+     .help = N_("some data")
+    },
+    {.name = "data2",
+     .type = VSH_OT_DATA,
+     .completer = vshCompleteFakeCommandData2,
+     .help = N_("some more data")
+    },
+    {.name = "string1",
+     .type = VSH_OT_STRING,
+     .completer = vshCompleteFakeCommandStr1,
+     .help = N_("a string")
+    },
+    {.name = "string2",
+     .type = VSH_OT_STRING,
+     .completer = vshCompleteFakeCommandStr2,
+     .help = N_("another string")
+    },
+    {.name = "string3",
+     .type = VSH_OT_STRING,
+     .help = N_("another string")
+    },
+    {.name = "file",
+     .type = VSH_OT_STRING,
+     .completer_flags = VSH_COMPLETE_AS_FILE,
+     .help = N_("a string")
+    },
+    {.name = NULL}
+};
+
+/* Used for testing completion
+ */
+static bool
+cmdFakeCommand(vshControl *ctl ATTRIBUTE_UNUSED,
+               const vshCmd *cmd ATTRIBUTE_UNUSED)
+{
+    vshCmdOpt *opt = cmd->opts;
+    if (opt) {
+        do {
+            if (opt->data)
+                vshPrint(NULL, "%s: %s\n", opt->def->name, opt->data);
+            else
+                vshPrint(NULL, "%s: true\n", opt->def->name);
+        } while ((opt = opt->next));
+    }
+
+    return true;
+}
+
+/*
  * "quit" command
  */
 static const vshCmdInfo info_quit[] = {
@@ -1931,7 +2023,9 @@ vshExtractLinePart(vshControl *ctl, vshCommandParser *parser,
 
     if (tok_type == VSH_TK_ERROR) {
         ret = VSH_LINE_STATE_TOK_ERR;
-        *opt = NULL;
+        *tok_out = vshStrdup(ctl, tok);
+        /* attempt to determine the option anyway... */
+        *opt = vshCmddefGetData(*cmd, opts_need_arg, opts_seen);
         goto cleanup;
     } else if (tok_type == VSH_TK_END) {
         ret = VSH_LINE_STATE_LINE_DONE;
@@ -2274,6 +2368,8 @@ vshCommandStringGetArg(vshControl *ctl, vshCommandParser *parser,
             if (*p == '\0') {
                 if (raise_err)
                     vshError(ctl, "%s", _("dangling \\"));
+                else
+                    *(*res + sz) = '\0';
 
                 return VSH_TK_ERROR;
             }
@@ -2289,6 +2385,8 @@ vshCommandStringGetArg(vshControl *ctl, vshCommandParser *parser,
     if (double_quote) {
         if (raise_err)
             vshError(ctl, "%s", _("missing \""));
+        else
+            *(*res + sz) = '\0';
 
         return VSH_TK_ERROR;
     }
@@ -3014,27 +3112,160 @@ 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;
+    static bool continue_from_error = false;
+    static char *last_tok = NULL;
+    static bool data_only = false;
+    vshCommandParser parser;
 
     if (!state) {
-        /* determine command name */
-        char *p;
-        char *cmdname;
+        char *tok = NULL;
+        vshLineExtractionState line_state;
+        int data_only_track = 0;
 
-        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;
+        VIR_FREE(last_tok);
+        continue_from_error = false;
+        data_only = false;
 
-        cmdname = vshCalloc(NULL, (p - rl_line_buffer) + 1, 1);
-        memcpy(cmdname, rl_line_buffer, p - rl_line_buffer);
+        /* reset the parser */
+        memset(&parser, 0, sizeof(vshCommandParser));
+        parser.pos = rl_line_buffer;
+        parser.getNextArg = vshCommandStringGetArg;
 
-        cmd = vshCmddefSearch(cmdname);
-        list_index = 0;
-        len = strlen(text);
-        VIR_FREE(cmdname);
+        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;
+            }
+
+
+            if (line_state == VSH_LINE_STATE_CMD_DONE) {
+                cmd = NULL;
+                data_only_track = 0;
+            } else if (line_state == VSH_LINE_STATE_DATA_ONLY) {
+                data_only_track++;
+            } else if (data_only_track) {
+                data_only_track++;
+            }
+
+            VIR_FREE(tok);
+
+            line_state = vshExtractLinePart(NULL, &parser, &opts_seen,
+                                            &opts_need_arg, &tok, &cmd,
+                                            &curr_opt, false, 1);
+        }
+
+        if (data_only_track && (data_only_track > 1 || len == 0))
+            data_only = true;
+
+        if (line_state == VSH_LINE_STATE_TOK_ERR) {
+            /* we're here either because of a dangling
+             * backslash or a missing quote */
+            continue_from_error = true;
+            last_tok = tok;
+        } else {
+            VIR_FREE(tok);
+        }
+
+        if (last_arg_opt && len > 0) {
+            if (last_arg_opt->type != VSH_OT_DATA &&
+                    last_arg_opt->type != VSH_OT_ARGV) {
+                if (text[0] == '-' && !text[1]) {
+                    /* this ensures that completion on '-' works properly */
+                    int opt_ind = -1;
+                    for (opt_ind = 0; cmd->opts[opt_ind].name; opt_ind++) {
+                        if (last_arg_opt == &cmd->opts[opt_ind])
+                            break;
+                    }
+                    opts_seen &= ~(1 << opt_ind);
+                    opts_need_arg &= ~(1 << opt_ind);
+                    last_arg_opt = NULL;
+                } else {
+                    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)
@@ -3043,14 +3274,146 @@ vshReadlineOptionsGenerator(const char *text, int state)
     if (!cmd->opts)
         return NULL;
 
+    if (waiting_for_flag_arg) {
+        char* res;
+        if (continue_from_error)
+            res = vshDelegateToCustomCompleter(curr_opt, last_tok, substate);
+        else
+            res = vshDelegateToCustomCompleter(curr_opt, text, substate);
+
+        substate++;
+        /* if we're in a flag's argument, we don't
+         * want to show other flags */
+
+        if (res && strchr(res, ' ')) {
+            /* quote matches with spaces */
+            char *orig = res;
+            int orig_len = strlen(orig);
+            res = vshMalloc(NULL, orig_len + 3);
+            snprintf(res, orig_len + 3, "\"%s\"", orig);
+            VIR_FREE(orig);
+        }
+
+        if (res && continue_from_error) {
+            char *orig = res;
+            int orig_len = strlen(orig);
+            if (strchr(last_tok, ' ')) {
+                int part_len = strlen(last_tok);
+                char *start_pos = orig + part_len;
+                int new_len;
+
+                if (len == 0)
+                    start_pos++;
+
+                new_len = strlen(start_pos);
+
+                res = vshMalloc(NULL, orig_len - part_len + 1);
+                vshStrncpy(NULL, res, start_pos, new_len, new_len + 1);
+            } else if (res[0] == '"') {
+                /* if we don't have a space, that we're actually
+                 * completing normally so far -- we just need to
+                 * remove the initial quote */
+                res = vshMalloc(NULL, orig_len);
+                vshStrncpy(NULL, res, orig + 1, orig_len - 1, orig_len);
+            }
+            VIR_FREE(orig);
+        }
+
+        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 */
+            if (continue_from_error)
+                res = vshDelegateToCustomCompleter(opt, last_tok, substate);
+            else
+                res = vshDelegateToCustomCompleter(opt, text, substate);
+            substate++;
+            if (res) {
+                if (strchr(res, ' ')) {
+                    /* quote matches with spaces */
+                    char *orig = res;
+                    int orig_len = strlen(orig);
+                    res = vshMalloc(NULL, orig_len + 3);
+                    snprintf(res, orig_len + 3, "\"%s\"", orig);
+                    VIR_FREE(orig);
+                }
+
+                if (continue_from_error) {
+                    char *orig = res;
+                    int orig_len = strlen(orig);
+                    if (strchr(last_tok, ' ')) {
+                        int part_len = strlen(last_tok);
+                        char *start_pos = orig + part_len;
+                        int new_len;
+
+                        if (len == 0)
+                            start_pos++;
+
+                        new_len = strlen(start_pos);
+
+                        res = vshMalloc(NULL, orig_len - part_len + 1);
+                        vshStrncpy(NULL, res, start_pos, new_len, new_len + 1);
+                    } else if (res[0] == '"') {
+                        /* if we don't have a space, that we're actually
+                         * completing normally so far -- we just need to
+                         * remove the initial quote */
+                        res = vshMalloc(NULL, orig_len);
+                        vshStrncpy(NULL, res, orig + 1, orig_len - 1, orig_len);
+                    }
+                    VIR_FREE(orig);
+                }
+
+                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 || continue_from_error) && text[0] != '-')
+                    return NULL;
+                else {
+                    list_index++;
+                    continue;
+                }
+            }
+        }
+
         list_index++;
 
-        if (opt->type == VSH_OT_DATA || opt->type == VSH_OT_ARGV)
-            /* ignore non --option */
+        /* don't complete flags if we're past a -- */
+        if (data_only)
+            continue;
+
+        /* don't complete flags if we're in the middle of
+         * completing a quoted data string */
+        if (continue_from_error)
+            continue;
+
+        /* 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) {
@@ -3078,6 +3441,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;
 }
 
@@ -3096,7 +3463,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 */
@@ -3165,6 +3533,96 @@ vshReadline(vshControl *ctl ATTRIBUTE_UNUSED, const char *prompt)
     return readline(prompt);
 }
 
+
+/*
+ * "complete" command
+ */
+static const vshCmdInfo info_complete[] = {
+    {.name = "help",
+     .data = N_("complete the given command")
+    },
+    {.name = "desc",
+     .data = N_("Complete the given input as if "
+                "readline was doing tab-completion "
+                "in the interactive shell")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_complete[] = {
+    {.name = "input",
+     .type = VSH_OT_DATA,
+     .flags = VSH_OFLAG_REQ,
+     .help = N_("pass in the line to be completed in quotes"),
+    },
+    {.name = "rest",
+     .type = VSH_OT_ARGV,
+     .flags = VSH_OFLAG_REQ,
+     .help = N_("pass in the line to be completed following "
+                " a '--' without quotes")
+    },
+    {.name = NULL}
+};
+
+static bool
+cmdComplete(vshControl *ctl, const vshCmd *cmd)
+{
+    const char *input;
+    const char *full_line;
+    const vshCmdOpt *opt = NULL;
+    virBuffer input_buff = VIR_BUFFER_INITIALIZER;
+    int line_len;
+    const char *text;
+    char **matches;
+    char *match;
+
+    if (vshCommandOptStringReq(ctl, cmd, "input", &input) < 0)
+        return false;
+
+    virBufferAdd(&input_buff, input, -1);
+
+    while ((opt = vshCommandOptArgv(cmd, opt))) {
+        virBufferAddLit(&input_buff, " ");
+        if (opt->data[0])
+            virBufferAdd(&input_buff, opt->data, -1);
+    }
+
+    if (virBufferError(&input_buff)) {
+        vshPrint(ctl, "%s", _("Failed to allocate XML buffer"));
+        return false;
+    }
+
+    full_line = virBufferContentAndReset(&input_buff);
+    line_len = strlen(full_line);
+
+    /* intialize the readline line buffer */
+    rl_extend_line_buffer(line_len);
+    rl_line_buffer = vshStrdup(ctl, full_line);
+
+    text = strrchr(rl_line_buffer, ' ');
+    if (!text)
+        text = rl_line_buffer;
+    else
+        text += 1; // skip the space
+
+    matches = vshReadlineCompletion(text, (int) (text - rl_line_buffer),
+                                    line_len);
+
+    if (matches) {
+        size_t i;
+        for (i = 0; (match = matches[i]); i++) {
+            vshPrint(ctl, "%s\n", match);
+            VIR_FREE(match); /* readline normally frees matches itself */
+        }
+
+        VIR_FREE(matches);
+    }
+
+    VIR_FREE(full_line);
+    return true;
+}
+
+
 #else /* !WITH_READLINE */
 
 static int
@@ -3603,12 +4061,26 @@ static const vshCmdDef virshCmds[] = {
      .info = info_cd,
      .flags = VSH_CMD_FLAG_NOCONNECT
     },
+#if WITH_READLINE
+    {.name = "complete",
+     .handler = cmdComplete,
+     .opts = opts_complete,
+     .info = info_complete,
+     .flags = VSH_CMD_FLAG_NOCONNECT,
+    },
+#endif /* WITH_READLINE */
     {.name = "connect",
      .handler = cmdConnect,
      .opts = opts_connect,
      .info = info_connect,
      .flags = VSH_CMD_FLAG_NOCONNECT
     },
+    {.name = "fake-command",
+     .handler = cmdFakeCommand,
+     .opts = opts_fake_command,
+     .info = info_fake_command,
+     .flags = VSH_CMD_FLAG_NOCONNECT
+    },
     {.name = "echo",
      .handler = cmdEcho,
      .opts = opts_echo,
diff --git a/tools/virsh.h b/tools/virsh.h
index 3e0251b..227e4e8 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
  *
@@ -411,6 +416,13 @@ char *_vshStrdup(vshControl *ctl, const char *s, const char *filename,
 # define realloc use_vshRealloc_instead_of_realloc
 # define strdup use_vshStrdup_instead_of_strdup
 
+# define vshStrncpy(_ctl, _dest, _src, _n, _destsize) \
+    _vshStrncpy(_ctl, _dest, _src, _n, _destsize, __FILE__, __LINE__)
+
+char *_vshStrncpy(vshControl *ctl, char *dest, const char *src, size_t n,
+                  size_t destsize, const char *filename, int line);
+
+
 /* Macros to help dealing with mutually exclusive options. */
 
 /* VSH_EXCLUSIVE_OPTIONS_EXPR:
-- 
1.8.3.2

--
libvir-list mailing list
libvir-list@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/libvir-list




[Index of Archives]     [Virt Tools]     [Libvirt Users]     [Lib OS Info]     [Fedora Users]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]