Bring in the improved internal API for the CLI.
authorLuigi Rizzo <rizzo@icir.org>
Tue, 14 Nov 2006 15:23:35 +0000 (15:23 +0000)
committerLuigi Rizzo <rizzo@icir.org>
Tue, 14 Nov 2006 15:23:35 +0000 (15:23 +0000)
WATCH OUT: this changes the binary interface (ABI) for modules,
so e.g. users of g729 codecs need a rebuilt module (but read below).

The new way to write CLI handlers is described in detail in cli.h,
and there are a few converted handlers in cli.c, look for NEW_CLI.

After converting a couple of commands i am convinced that
it is reasonably convenient to use, and it makes it easier to fix the
pending CLI issues.

On passing, note a bug with the current 'complete' architecture:
if a command is a prefix of multiple CLI entries, we miss some
of the possible options. As an example, "core set debug" can
continue with "channel" from one CLI entry, and "off" or "atleast"
from another one.

We address this problem in a separate commit
(when i have figured out a fix, that is).

ABI issues:
I asked Kevin if it was ok to make this change and he said yes.
While it would have been possible to make the change without breaking
the module ABI, the code would have been more convoluted.

I am happy to restore the old ABI (while still being able
to use the "new style" handlers) if there is demand.

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@47606 65c4cc65-6c06-0410-ace0-fbb531ad65f3

include/asterisk/cli.h
main/cli.c

index e2a6f81..050fe87 100644 (file)
@@ -44,9 +44,106 @@ void ast_cli(int fd, char *fmt, ...)
 
 #define AST_CLI_COMPLETE_EOF   "_EOF_"
 
-/*! \brief A command line entry */
+/*!
+   CLI commands are described by a struct ast_cli_entry that contains
+   all the components for their implementation.
+   In the "old-style" format, the record must contain:
+   - a NULL-terminated array of words constituting the command, e.g.
+       { "set", "debug", "on", NULL },
+   - a summary string (short) and a usage string (longer);
+   - a handler which implements the command itself, invoked with
+     a file descriptor and argc/argv as typed by the user
+   - a 'generator' function which, given a partial string, can
+     generate legal completions for it.
+   An example is
+
+       int old_setdebug(int fd, int argc, char *argv[]);
+       char *dbg_complete(const char *line, const char *word, int pos, int n);
+
+       { { "set", "debug", "on", NULL }, do_setdebug, "Enable debugging",
+       set_debug_usage, dbg_complete },
+
+   In the "new-style" format, all the above functionalities are implemented
+   by a single function, and the arguments tell which output is required.
+
+   NOTE: ideally, the new-style handler would have a different prototype,
+   i.e. something like
+       int new_setdebug(const struct ast_cli *e, int function,
+           int fd, int argc, char *argv[],     // handler args
+           int n, int pos, const char *line, const char *word // -complete args)
+   but at this moment we want to help the transition from old-style to new-style
+   functions so we keep the same interface and override some of the traditional
+   arguments.
+
+   To help the transition, a new-style entry has the same interface as the old one,
+   but it is declared as follows:
+
+       int new_setdebug(int fd, int argc, char *argv[]);
+
+       ...
+       // this is how we create the entry to register
+       NEW_CLI(new_setdebug, "short description")
+       ...
+
+   Called with the default arguments (argc > 0), the new_handler implements
+   the command as before.
+   A negative argc indicates one of the other functions, namely
+   generate the usage string, the full command, or implement the generator.
+   As a trick to extend the interface while being backward compatible,
+   argv[-1] points to a struct ast_cli_args, and, for the generator,
+   argv[0] is really a pointer to a struct ast_cli_args.
+   The return string is obtained by casting the result to char *
+
+   An example of new-style handler is the following
+
+\code
+static int test_new_cli(int fd, int argc, char *argv[])
+{
+        struct ast_cli_entry *e = (struct ast_cli_entry *)argv[-1];
+        struct ast_cli_args *a;
+       static char *choices = { "one", "two", "three", NULL };
+
+        switch(argc) {
+        case CLI_USAGE:
+                return (int)
+                       "Usage: do this well <arg>\n"
+                       "       typically multiline with body indented\n";
+
+        case CLI_CMD_STRING:
+                return (int)"do this well";
+
+        case CLI_GENERATE:
+                a = (struct ast_cli_args *)argv[0];
+                if (a->pos > e->args)
+                        return NULL;
+               return ast_cli_complete(a->word, choices, a->n);
+
+        default:        
+                // we are guaranteed to be called with argc >= e->args;
+                if (argc > e->args + 1) // we accept one extra argument
+                        return RESULT_SHOWUSAGE;
+                ast_cli(fd, "done this well for %s\n", e->args[argc-1]);
+                return RESULT_SUCCESS;
+        }
+}
+
+\endcode
+ *
+ */
+
+/*! \brief calling arguments for new-style handlers */
+enum ast_cli_fn {
+       CLI_USAGE = -1,         /* return the usage string */
+       CLI_CMD_STRING = -2,    /* return the command string */
+       CLI_GENERATE = -3,      /* behave as 'generator', remap argv to struct ast_cli_args */
+};
+
+typedef int (*old_cli_fn)(int fd, int argc, char *argv[]);
+
+/*! \brief descriptor for a cli entry */
 struct ast_cli_entry {
-       char * const cmda[AST_MAX_CMD_LEN];
+       char * const cmda[AST_MAX_CMD_LEN];     /*!< words making up the command.
+               * set the first entry to NULL for a new-style entry. */
        /*! Handler for the command (fd for output, # of args, argument list).
          Returns RESULT_SHOWUSAGE for improper arguments.
          argv[] has argc 'useful' entries, and an additional NULL entry
@@ -56,10 +153,8 @@ struct ast_cli_entry {
          that this memory is deallocated after the handler returns.
         */
        int (*handler)(int fd, int argc, char *argv[]);
-       /*! Summary of the command (< 60 characters) */
-       const char *summary;
-       /*! Detailed usage information */
-       const char *usage;
+       const char *summary; /*!< Summary of the command (< 60 characters) */
+       const char *usage; /*!< Detailed usage information */
        /*! Generate the n-th (starting from 0) possible completion
          for a given 'word' following 'line' in position 'pos'.
          'line' and 'word' must not be modified.
@@ -70,22 +165,34 @@ struct ast_cli_entry {
         */
        char *(*generator)(const char *line, const char *word, int pos, int n);
        struct ast_cli_entry *deprecate_cmd;
-       /*! For keeping track of usage */
-       int inuse;
-       struct module *module;  /*! module this belongs to */
+       int inuse; /*!< For keeping track of usage */
+       struct module *module;  /*!< module this belongs to */
        char *_full_cmd;        /* built at load time from cmda[] */
        /* This gets set in ast_cli_register()
          It then gets set to something different when the deprecated command
          is run for the first time (ie; after we warn the user that it's deprecated)
         */
+       int args;               /*!< number of non-null entries in cmda */
+       char *command;          /*!< command, non-null for new-style entries */
        int deprecated;
        char *_deprecated_by;   /* copied from the "parent" _full_cmd, on deprecated commands */
        /*! For linking */
        AST_LIST_ENTRY(ast_cli_entry) list;
 };
 
+#define NEW_CLI(fn, txt)       { .handler = (old_cli_fn)fn, .summary = txt }
+
+/* argument for new-style CLI handler */
+struct ast_cli_args {
+       char fake[4];           /* a fake string, in the first position, for safety */
+       const char *line;       /* the current input line */
+       const char *word;       /* the word we want to complete */
+       int pos;                /* position of the word to complete */
+       int n;                  /* the iteration count (n-th entry we generate) */
+};
+
 /*!
- * \brief Helper function to generate cli entries from a NULL-terminated array.
+ * Helper function to generate cli entries from a NULL-terminated array.
  * Returns the n-th matching entry from the array, or NULL if not found.
  * Can be used to implement generate() for static entries as below
  * (in this example we complete the word in position 2):
index e3a5ffe..5949085 100644 (file)
@@ -111,17 +111,6 @@ static char verbose_help[] =
 "       no messages should be displayed. Equivalent to -v[v[v...]]\n"
 "       on startup\n";
 
-static char debug_help[] = 
-"Usage: core set debug <level> [filename]\n"
-"       Sets level of core debug messages to be displayed.  0 means\n"
-"       no messages should be displayed.  Equivalent to -d[d[d...]]\n"
-"       on startup.  If filename is specified, debugging will be\n"
-"       limited to just that file.\n";
-
-static char nodebug_help[] = 
-"Usage: core set debug off\n"
-"       Turns off core debug messages.\n";
-
 static char logger_mute_help[] = 
 "Usage: logger mute\n"
 "       Disables logging output to the current console, making it possible to\n"
@@ -245,55 +234,61 @@ static int handle_verbose(int fd, int argc, char *argv[])
 
 static int handle_set_debug(int fd, int argc, char *argv[])
 {
+       struct ast_cli_entry *e = (struct ast_cli_entry *)argv[-1];
        int oldval = option_debug;
        int newlevel;
        int atleast = 0;
        char *filename = '\0';
-
-       /* 'core set debug <level>'
-        * 'core set debug <level> <fn>'
-        * 'core set debug atleast <level>'
-        * 'core set debug atleast <level> <fn>'
+       static char *choices[] = { "off", "atleast", NULL };
+       struct ast_cli_args *a;
+
+       switch (argc) {
+       case CLI_CMD_STRING:
+               return (int)"core set debug";
+
+       case CLI_USAGE:
+               return (int)
+                       "Usage: core set debug [atleast] <level> [filename]\n"
+                       "       core set debug off\n"
+                       "       Sets level of core debug messages to be displayed. 0 or 'off' means\n"
+                       "       no messages should be displayed.  Equivalent to -d[d[d...]]\n"
+                       "       on startup.  If filename is specified, debugging will be\n"
+                       "       limited to just that file.\n";
+
+       case CLI_GENERATE:
+               a = (struct ast_cli_args *)argv[0];
+               if (a->pos > e->args)
+                       return NULL;
+               return (int)ast_cli_complete(a->word, choices, a->n);
+       }
+       /* all the above return, so we proceed with the handler.
+        * we are guaranteed to be called with argc >= e->args;
         */
-       if ((argc < 4) || (argc > 6))
+
+       if (argc < e->args + 1)
                return RESULT_SHOWUSAGE;
 
-       if (!strcasecmp(argv[3], "atleast"))
+       if (argc == e->args + 1 && !strcasecmp(argv[e->args], "off")) {
+               newlevel = 0;
+               goto done;
+       }
+       if (!strcasecmp(argv[e->args], "atleast"))
                atleast = 1;
+       if (argc > e->args + atleast + 2)
+               return RESULT_SHOWUSAGE;
+       if (sscanf(argv[e->args + atleast], "%d", &newlevel) != 1)
+               return RESULT_SHOWUSAGE;
 
-       if (!atleast) {
-               if (argc > 5)
-                       return RESULT_SHOWUSAGE;
-
-               if (sscanf(argv[3], "%d", &newlevel) != 1)
-                       return RESULT_SHOWUSAGE;
-
-               if (argc == 4) {
-                       debug_filename[0] = '\0';
-               } else {
-                       filename = argv[4];
-                       ast_copy_string(debug_filename, filename, sizeof(debug_filename));
-               }
-
-               option_debug = newlevel;
+       if (argc == e->args + atleast + 1) {
+               debug_filename[0] = '\0';
        } else {
-               if (argc < 5 || argc > 6)
-                       return RESULT_SHOWUSAGE;
-
-               if (sscanf(argv[4], "%d", &newlevel) != 1)
-                       return RESULT_SHOWUSAGE;
-
-               if (argc == 5) {
-                       debug_filename[0] = '\0';
-               } else {
-                       filename = argv[5];
-                       ast_copy_string(debug_filename, filename, sizeof(debug_filename));
-               }
-
-               if (newlevel > option_debug)
-                       option_debug = newlevel;
+               ast_copy_string(debug_filename, argv[e->args + atleast + 1], sizeof(debug_filename));
        }
 
+done:
+       if (!atleast || newlevel > option_debug)
+               option_debug = newlevel;
+
        if (oldval > 0 && option_debug == 0)
                ast_cli(fd, "Core debug is now OFF\n");
        else if (option_debug > 0) {
@@ -407,10 +402,6 @@ static int modlist_modentry(const char *module, const char *description, int use
        return 0;
 }
 
-static char modlist_help[] =
-"Usage: module show [like keyword]\n"
-"       Shows Asterisk modules currently in use, and usage statistics.\n";
-
 static char uptime_help[] =
 "Usage: core show uptime [seconds]\n"
 "       Shows Asterisk uptime information.\n"
@@ -483,17 +474,39 @@ static int handle_showuptime(int fd, int argc, char *argv[])
        return RESULT_SUCCESS;
 }
 
-/* core show modules [like keyword] */
 static int handle_modlist(int fd, int argc, char *argv[])
 {
-       char *like = "";
-       if (argc != 2 && argc != 4)
-               return RESULT_SHOWUSAGE;
-       else if (argc == 4) {
-               if (strcmp(argv[2],"like")) 
-                       return RESULT_SHOWUSAGE;
-               like = argv[3];
+       struct ast_cli_entry *e = (struct ast_cli_entry *)argv[-1];
+       char *like;
+       struct ast_cli_args *a;
+
+       switch(argc) {
+       case CLI_CMD_STRING:
+               return (int)"module show";
+
+       case CLI_USAGE:
+               return (int)
+                       "Usage: module show [like keyword]\n"
+                       "       Shows Asterisk modules currently in use, and usage statistics.\n";
+
+       case CLI_GENERATE:
+               a = (struct ast_cli_args *)argv[0];
+               if (a->pos == e->args)
+                       return (int)(a->n == 0 ? strdup("like") : NULL);
+               else if (a->pos == e->args+1 && strcasestr(a->line," like "))
+                       return (int)ast_module_helper(a->line, a->word, a->pos, a->n, a->pos, 0);
+               else
+                       return (int)NULL;
        }
+       /* all the above return, so we proceed with the handler.
+        * we are guaranteed to have argc >= e->args
+        */
+       if (argc == e->args)
+               like = "";
+       else if (argc == e->args + 2 && !strcmp(argv[e->args],"like"))
+               like = argv[e->args + 1];
+       else
+               return RESULT_SHOWUSAGE;
                
        ast_mutex_lock(&climodentrylock);
        climodentryfd = fd; /* global, protected by climodentrylock */
@@ -981,11 +994,6 @@ static char *complete_mod_3(const char *line, const char *word, int pos, int sta
        return ast_module_helper(line, word, pos, state, 2, 1);
 }
 
-static char *complete_mod_4(const char *line, const char *word, int pos, int state)
-{
-       return ast_module_helper(line, word, pos, state, 3, 0);
-}
-
 static char *complete_fn(const char *line, const char *word, int pos, int state)
 {
        char *c;
@@ -1129,13 +1137,7 @@ static struct ast_cli_entry cli_cli[] = {
        handle_core_set_debug_channel, "Enable/disable debugging on a channel",
        debugchan_help, complete_ch_5, &cli_debug_channel_deprecated },
 
-       { { "core", "set", "debug", NULL },
-       handle_set_debug, "Set level of debug chattiness",
-       debug_help },
-
-       { { "core", "set", "debug", "off", NULL },
-       handle_nodebug, "Turns off debug chattiness",
-       nodebug_help },
+       NEW_CLI(handle_set_debug, "Set level of debug chattiness"),
 
        { { "core", "set", "verbose", NULL },
        handle_verbose, "Set level of verboseness",
@@ -1153,13 +1155,7 @@ static struct ast_cli_entry cli_cli[] = {
        handle_logger_mute, "Toggle logging output to a console",
        logger_mute_help },
 
-       { { "module", "show", NULL },
-       handle_modlist, "List modules and info",
-       modlist_help },
-
-       { { "module", "show", "like", NULL },
-       handle_modlist, "List modules and info",
-       modlist_help, complete_mod_4 },
+       NEW_CLI(handle_modlist, "List modules and info"),
 
        { { "module", "load", NULL },
        handle_load, "Load a module by name",
@@ -1309,6 +1305,14 @@ static int __ast_cli_unregister(struct ast_cli_entry *e, struct ast_cli_entry *e
                AST_LIST_REMOVE(&helpers, e, list);
                AST_LIST_UNLOCK(&helpers);
                free(e->_full_cmd);
+               e->_full_cmd = NULL;
+               if (e->command) {
+                       /* this is a new-style entry. Reset fields and free memory. */
+                       ((char **)e->cmda)[0] = NULL;
+                       free(e->command);
+                       e->command = NULL;
+                       e->usage = NULL;
+               }
        }
        return 0;
 }
@@ -1317,8 +1321,29 @@ static int __ast_cli_register(struct ast_cli_entry *e, struct ast_cli_entry *ed)
 {
        struct ast_cli_entry *cur;
        char fulle[80] ="";
-       int lf, ret = -1;
-       
+       int i, lf, ret = -1;
+
+       if (e->cmda[0] == NULL) {       /* new style entry, run the handler to init fields */
+               char *args[2] = { (char *)e, NULL };
+               char *s = (char *)(e->handler(-1, CLI_CMD_STRING, args+1));
+               char **dst = (char **)e->cmda;  /* need to cast as the entry is readonly */
+
+               s = ast_skip_blanks(s);
+               s = e->command = ast_strdup(s);
+               for (i=0; !ast_strlen_zero(s) && i < AST_MAX_CMD_LEN-1; i++) {
+                       *dst++ = s;     /* store string */
+                       s = ast_skip_nonblanks(s);
+                       if (*s == '\0') /* we are done */
+                               break;
+                       *s++ = '\0';
+                       s = ast_skip_blanks(s);
+               }
+               *dst++ = NULL;
+               e->usage = (char *)(e->handler(-1, CLI_USAGE, args+1));
+       }
+       for (i = 0; e->cmda[i]; i++)
+               ;
+       e->args = i;
        ast_join(fulle, sizeof(fulle), e->cmda);
        AST_LIST_LOCK(&helpers);
        
@@ -1432,7 +1457,7 @@ static int help1(int fd, char *match[], int locked)
                        continue;
                if (match && strncasecmp(matchstr, e->_full_cmd, len))
                        continue;
-               ast_cli(fd, "%25.25s  %s\n", e->_full_cmd, e->summary);
+               ast_cli(fd, "%25.25s  %s\n", e->_full_cmd, S_OR(e->summary, "<no description available>"));
                found++;
        }
        AST_LIST_UNLOCK(&helpers);
@@ -1622,7 +1647,7 @@ static char *__ast_cli_generator(const char *text, const char *word, int state,
        }
        if (lock)
                AST_LIST_LOCK(&helpers);
-       while ((e = cli_next(&i))) {
+       while ( (e = cli_next(&i)) ) {
                int lc = strlen(e->_full_cmd);
                if (e->_full_cmd[0] != '_' && lc > 0 && matchlen <= lc &&
                                !strncasecmp(matchstr, e->_full_cmd, matchlen)) {
@@ -1632,11 +1657,30 @@ static char *__ast_cli_generator(const char *text, const char *word, int state,
                                break;
                        }
                } else if (!strncasecmp(matchstr, e->_full_cmd, lc) && matchstr[lc] < 33) {
-                       /* We have a command in its entirity within us -- theoretically only one
-                          command can have this occur */
+                       /* This entry is a prefix of the command string entered
+                        * (only one entry in the list should have this property).
+                        * Run the generator if one is available. In any case we are done.
+                        */
                        if (e->generator)
                                ret = e->generator(matchstr, word, argindex, state);
-                       break;
+                       else if (e->command) {  /* new style command */
+                               /* prepare fake arguments for the generator.
+                                * argv[-1] is the cli entry we use,
+                                * argv[0] is a pointer to the generator arguments,
+                                *   with a fake string '-' at the beginning so we can
+                                *   dereference it as a string with no trouble,
+                                *   and then the usual NULL terminator.
+                                */
+                               struct ast_cli_args a = {
+                                       .fake = "-",
+                                       .line = matchstr, .word = word,
+                                       .pos = argindex,
+                                       .n = state };
+                               char *args[] = { (char *)e, (char *)&a, NULL };
+                               ret = (char *)e->handler(-1, CLI_GENERATE, args + 1);
+                       }
+                       if (ret)
+                               break;
                }
        }
        if (lock)
@@ -1652,24 +1696,28 @@ char *ast_cli_generator(const char *text, const char *word, int state)
 
 int ast_cli_command(int fd, const char *s)
 {
-       char *argv[AST_MAX_ARGS];
+       char *args[AST_MAX_ARGS + 1];
        struct ast_cli_entry *e;
        int x;
        char *dup;
        int tws;
        
-       if (!(dup = parse_args(s, &x, argv, sizeof(argv) / sizeof(argv[0]), &tws)))
+       if (!(dup = parse_args(s, &x, args + 1, AST_MAX_ARGS, &tws)))
                return -1;
 
        /* We need at least one entry, or ignore */
        if (x > 0) {
                AST_LIST_LOCK(&helpers);
-               e = find_cli(argv, 0);
+               e = find_cli(args + 1, 0);
                if (e)
                        e->inuse++;
                AST_LIST_UNLOCK(&helpers);
                if (e) {
-                       switch(e->handler(fd, x, argv)) {
+                       /* within calling the handler, argv[-1] contains a pointer
+                        * to the cli entry, and the array is null-terminated
+                        */
+                       args[0] = (char *)e;
+                       switch(e->handler(fd, x, args + 1)) {
                        case RESULT_SHOWUSAGE:
                                if (e->usage)
                                        ast_cli(fd, "%s", e->usage);
@@ -1686,7 +1734,7 @@ int ast_cli_command(int fd, const char *s)
                                break;
                        }
                } else 
-                       ast_cli(fd, "No such command '%s' (type 'help' for help)\n", find_best(argv));
+                       ast_cli(fd, "No such command '%s' (type 'help' for help)\n", find_best(args + 1));
                if (e)
                        ast_atomic_fetchadd_int(&e->inuse, -1);
        }