Improve command-line parsing (issue #59)

Emit HINT log message about using "help" command

Support alternatives syntax "word|word|..." in CLI schema

Better return values from cli_parse()
This commit is contained in:
Andrew Bettison 2013-04-09 17:18:37 +09:30
parent b826ac1f1b
commit 5bbdef5587
4 changed files with 161 additions and 107 deletions

192
cli.c
View File

@ -1,5 +1,25 @@
/*
Serval DNA command-line functions
Copyright (C) 2010-2013 Serval Project, Inc.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <stdio.h>
#include <strings.h>
#include <assert.h>
#include "cli.h"
#include "log.h"
#include "serval.h"
@ -49,6 +69,20 @@ int cli_usage_args(const int argc, const char *const *args, const struct cli_sch
return 0;
}
/* Returns 0 if a command is matched and parsed, with the results of the parsing in the '*parsed'
* structure.
*
* Returns 1 and logs an error if no command matches the argument list, contents of '*parsed' are
* undefined.
*
* Returns 2 if the argument list is ambiguous, ie, matches more than one command, contents of
* '*parsed' are undefined.
*
* Returns -1 and logs an error if the parsing fails due to an internal error (eg, malformed command
* schema), contents of '*parsed' are undefined.
*
* @author Andrew Bettison <andrew@servalproject.com>
*/
int cli_parse(const int argc, const char *const *args, const struct cli_schema *commands, struct cli_parsed *parsed)
{
int ambiguous = 0;
@ -63,24 +97,27 @@ int cli_parse(const int argc, const char *const *args, const struct cli_schema *
cmdpa.argc = argc;
cmdpa.labelc = 0;
cmdpa.varargi = -1;
const char *word = NULL;
const char *pattern = NULL;
unsigned arg = 0;
unsigned opt = 0;
while ((word = commands[cmd].words[opt])) {
//DEBUGF("cmd=%d opt=%d word='%s' args[arg=%d]='%s'", cmd, opt, word, arg, arg < argc ? args[arg] : "");
unsigned wordlen = strlen(word);
while ((pattern = commands[cmd].words[opt])) {
//DEBUGF("cmd=%d opt=%d pattern='%s' args[arg=%d]='%s'", cmd, opt, pattern, arg, arg < argc ? args[arg] : "");
unsigned patlen = strlen(pattern);
if (cmdpa.varargi != -1)
return WHYF("Internal error: commands[%d].word[%d]=\"%s\" - more words not allowed after \"...\"", cmd, opt, word);
return WHYF("Internal error: commands[%d].word[%d]=\"%s\" - more words not allowed after \"...\"", cmd, opt, commands[cmd].words[opt]);
/* These are the argument matching rules:
*
* "..." consumes all remaining arguments
*
* "word" consumes one argument that exactly matches "word", does not label it
* "word" consumes one argument that exactly matches "word", does not label it (this is the
* "simple" case in the code below; all other rules label something that matched)
*
* "\word" consumes one argument that exactly matches "word" and labels it "word"
* "word1|word2|...|wordN" consumes one argument that exactly matches "word1" or "word2" etc.
* or "wordN", labels it with the matched word (an empty alternative, eg "|word" does not
* match an empty argument)
*
* "[word]" consumes one argument if it exactly matches "word", records it with label
* "word"
* (as a result of the above rule, "|word" or "word|" or "|word|" consumes one argument that
* exactly matches "word" and labels it "word")
*
* "<label>" consumes exactly one argument "ANY", records it with label "label"
*
@ -93,6 +130,14 @@ int cli_parse(const int argc, const char *const *args, const struct cli_schema *
* "prefix<any>" consumes one argument "prefixANY", and records the text matching ANY with
* label "prefix"
*
* "[ANY]..." consumes all remaining arguments which match ANY, as defined below
*
* "[word]" consumes one argument if it exactly matches "word", records it with label
* "word"
*
* "[word1|word2|...|wordN]" consumes one argument if it exactly matches "word1" or "word2"
* etc. or "wordN", labels it with the matched word
*
* "[<label>]" consumes one argument "ANY" if available, records it with label "label"
*
* "[prefix=<any>]" consumes one argument "prefix=ANY" if available or two arguments
@ -104,72 +149,85 @@ int cli_parse(const int argc, const char *const *args, const struct cli_schema *
* "[prefix<any>]" consumes one argument "prefixANY" if available, records the text matching
* ANY with label "prefix"
*/
if (wordlen == 3 && word[0] == '.' && word[1] == '.' && word[2] == '.') {
if (patlen == 3 && pattern[0] == '.' && pattern[1] == '.' && pattern[2] == '.') {
cmdpa.varargi = arg;
arg = argc;
++opt;
} else {
int optional = 0;
int repeating = 0;
if (wordlen > 5 && word[0] == '[' && word[wordlen-4] == ']' && word[wordlen-3] == '.' && word[wordlen-2] == '.' && word[wordlen-1] == '.') {
if (patlen > 5 && pattern[0] == '[' && pattern[patlen-4] == ']' && pattern[patlen-3] == '.' && pattern[patlen-2] == '.' && pattern[patlen-1] == '.') {
optional = repeating = 1;
word += 1;
wordlen -= 5;
pattern += 1;
patlen -= 5;
}
else if (wordlen > 2 && word[0] == '[' && word[wordlen-1] == ']') {
else if (patlen > 2 && pattern[0] == '[' && pattern[patlen-1] == ']') {
optional = 1;
word += 1;
wordlen -= 2;
pattern += 1;
patlen -= 2;
}
const char *prefix = NULL;
unsigned prefixlen = 0;
char prefixarglen = 0;
unsigned oarg = arg;
const char *text = NULL;
const char *label = NULL;
unsigned labellen = 0;
const char *text = NULL;
const char *caret = strchr(word, '<');
unsigned oarg = arg;
if (wordlen > 2 && caret && word[wordlen-1] == '>') {
if ((prefixarglen = prefixlen = caret - word)) {
prefix = word;
if (prefixlen > 1 && (prefix[prefixlen-1] == '=' || prefix[prefixlen-1] == ' '))
--prefixarglen;
label = prefix;
labellen = prefixarglen;
if (arg < argc) {
unsigned arglen = strlen(args[arg]);
if (arglen >= prefixlen && strncmp(args[arg], prefix, prefixlen) == 0) {
text = args[arg++] + prefixlen;
} else if (arg + 1 < argc && arglen == prefixarglen && strncmp(args[arg], prefix, prefixarglen) == 0) {
++arg;
text = args[arg++];
const char *word;
unsigned wordlen = 0;
char simple = 0;
unsigned alt = 0;
for (word = pattern; word < &pattern[patlen]; word += wordlen + 1, ++alt) {
// Skip over empty "||word" alternative (but still count it).
if (*word == '|') {
wordlen = 0;
continue;
}
// Find end of "word|" alternative.
wordlen = 1;
while (&word[wordlen] < &pattern[patlen] && word[wordlen] != '|')
++wordlen;
// Skip remaining alternatives if we already got a match.
if (text)
continue;
// Look for a match.
const char *prefix = NULL;
unsigned prefixlen = 0;
char prefixarglen = 0;
const char *caret = strchr(word, '<');
if (wordlen > 2 && caret && word[wordlen-1] == '>') {
if ((prefixarglen = prefixlen = caret - word)) {
prefix = word;
if (prefixlen > 1 && (prefix[prefixlen-1] == '=' || prefix[prefixlen-1] == ' '))
--prefixarglen;
label = prefix;
labellen = prefixarglen;
if (arg < argc) {
unsigned arglen = strlen(args[arg]);
if (arglen >= prefixlen && strncmp(args[arg], prefix, prefixlen) == 0) {
text = args[arg++] + prefixlen;
} else if (arg + 1 < argc && arglen == prefixarglen && strncmp(args[arg], prefix, prefixarglen) == 0) {
++arg;
text = args[arg++];
}
}
} else {
label = &word[1];
labellen = wordlen - 2;
if (arg < argc)
text = args[arg++];
}
} else {
label = &word[1];
labellen = wordlen - 2;
if (arg < argc)
text = args[arg++];
}
} else if (wordlen > 1 && word[0] == '\\') {
if (strlen(args[arg]) == wordlen - 1 && strncmp(args[arg], &word[1], wordlen - 1) == 0) {
label = &word[1];
labellen = wordlen - 1;
text = args[arg++];
}
} else if (arg < argc && strlen(args[arg]) == wordlen && strncmp(args[arg], word, wordlen) == 0) {
if (optional) {
} else if (arg < argc && strlen(args[arg]) == wordlen && strncmp(args[arg], word, wordlen) == 0) {
simple = 1;
text = args[arg];
label = word;
labellen = wordlen;
++arg;
}
++arg;
}
assert(alt > 0);
if (arg == oarg && !optional)
break;
if (labellen && text) {
if (labellen && text && (optional || !simple || alt > 1)) {
if (cmdpa.labelc >= NELS(cmdpa.labelv))
return WHYF("Internal error: commands[%d].word[%d]=\"%s\" - label limit exceeded", cmd, opt, word);
return WHYF("Internal error: commands[%d].word[%d]=\"%s\" - label limit exceeded", cmd, opt, commands[cmd].words[opt]);
cmdpa.labelv[cmdpa.labelc].label = label;
cmdpa.labelv[cmdpa.labelc].len = labellen;
cmdpa.labelv[cmdpa.labelc].text = text;
@ -181,37 +239,33 @@ int cli_parse(const int argc, const char *const *args, const struct cli_schema *
}
}
//DEBUGF("cmd=%d opt=%d args[arg=%d]='%s'", cmd, opt, arg, arg < argc ? args[arg] : "");
if (!word && arg == argc) {
if (!pattern && arg == argc) {
/* A match! We got through the command definition with no internal errors and all literal
args matched and we have a proper number of args. If we have multiple matches, then note
that the call is ambiguous. */
args matched and we have a proper number of args. If we have multiple matches, then note
that the call is ambiguous. */
if (matched_cmd >= 0)
++ambiguous;
if (ambiguous == 1) {
WHY("Ambiguous command line call:");
WHY_argv(" ", argc, args);
WHY("Matches the following known command line calls:");
WHY_argv(" ", argc, commands[matched_cmd].words);
NOWHENCE(WHY_argv("Ambiguous command:", argc, args));
NOWHENCE(HINT("Matches the following:"));
NOWHENCE(HINT_argv(" ", argc, commands[matched_cmd].words));
}
if (ambiguous)
WHY_argv(" ", argc, commands[cmd].words);
NOWHENCE(HINT_argv(" ", argc, commands[cmd].words));
matched_cmd = cmd;
*parsed = cmdpa;
}
}
/* Don't process ambiguous calls */
if (ambiguous)
return -1;
return 2;
/* Complain if we found no matching calls */
if (matched_cmd < 0) {
if (argc) {
WHY("Unknown command line call:");
WHY_argv(" ", argc, args);
}
INFO("Use \"help\" command to see a list of valid commands");
return -1;
if (argc)
NOWHENCE(WHY_argv("Unknown command:", argc, args));
return 1;
}
return matched_cmd;
return 0;
}
void _debug_cli_parsed(struct __sourceloc __whence, const struct cli_parsed *parsed)

View File

@ -1,6 +1,6 @@
/*
Serval Distributed Numbering Architecture (DNA)
Copyright (C) 2010-2012 Paul Gardner-Stephen
Serval DNA command-line functions
Copyright (C) 2010-2013 Serval Project, Inc.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
@ -224,8 +224,9 @@ int parseCommandLine(const char *argv0, int argc, const char *const *args)
struct cli_parsed parsed;
int result = cli_parse(argc, args, command_line_options, &parsed);
if (result != -1) {
// Do not run the command if the configuration does not load ok
switch (result) {
case 0:
// Do not run the command if the configuration does not load ok.
if (((parsed.commands[parsed.cmdi].flags & CLIFLAG_PERMISSIVE_CONFIG) ? cf_reload_permissive() : cf_reload()) != -1)
result = cli_invoke(&parsed, NULL);
else {
@ -233,9 +234,17 @@ int parseCommandLine(const char *argv0, int argc, const char *const *args)
strbuf_append_argv(b, argc, args);
result = WHYF("configuration defective, not running command: %s", strbuf_str(b));
}
} else {
// Load configuration so that "unsupported command" log message can get out
break;
case 1:
case 2:
// Load configuration so that log messages can get out.
cf_reload_permissive();
NOWHENCE(HINTF("Run \"%s help\" for more information.", argv0 ? argv0 : "servald"));
break;
default:
// Load configuration so that log error messages can get out.
cf_reload_permissive();
break;
}
/* clean up after ourselves */
@ -1899,7 +1908,7 @@ int app_id_self(const struct cli_parsed *parsed, void *context)
int count=0;
a.packetTypeAndFlags=MDP_GETADDRS;
const char *arg = parsed->argc >= 2 ? parsed->args[1] : "";
const char *arg = parsed->labelc ? parsed->labelv[0].text : "";
if (!strcasecmp(arg,"self"))
a.addrlist.mode = MDP_ADDRLIST_MODE_SELF; /* get own identities */
else if (!strcasecmp(arg,"allpeers"))
@ -2359,14 +2368,8 @@ struct cli_schema command_line_options[]={
"Display command usage."},
{app_echo,{"echo","[-e]","[--]","...",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Output the supplied string."},
{app_log,{"log","debug","<message>",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Log the supplied message at DEBUG level."},
{app_log,{"log","info","<message>",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Log the supplied message at INFO level."},
{app_log,{"log","warn","<message>",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Log the supplied message at WARN level."},
{app_log,{"log","error","<message>",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Log the supplied message at ERROR level."},
{app_log,{"log","error|warn|hint|info|debug","<message>",NULL},CLIFLAG_PERMISSIVE_CONFIG,
"Log the supplied message at given level."},
{app_server_start,{"start",NULL}, 0,
"Start Serval Mesh node process with instance path taken from SERVALINSTANCE_PATH environment variable."},
{app_server_start,{"start","in","<instance path>",NULL}, 0,
@ -2426,17 +2429,9 @@ struct cli_schema command_line_options[]={
{app_rhizome_extract,{"rhizome","extract","file" KEYRING_PIN_OPTIONS,
"<manifestid>","[<filepath>]","[<bsk>]",NULL}, 0,
"Extract and decrypt a file from Rhizome and write it to the given path"},
{app_rhizome_delete,{"rhizome","delete","\\manifest",
"<manifestid>",NULL}, 0,
"Remove the manifest for the given bundle from the Rhizome store"},
{app_rhizome_delete,{"rhizome","delete","\\payload",
"<manifestid>",NULL}, 0,
"Remove the payload for the given bundle from the Rhizome store"},
{app_rhizome_delete,{"rhizome","delete","\\bundle",
"<manifestid>",NULL}, 0,
"Remove the manifest and payload for the given bundle from the Rhizome store"},
{app_rhizome_delete,{"rhizome","delete","\\file",
"<fileid>",NULL}, 0,
{app_rhizome_delete,{"rhizome","delete","manifest|payload|bundle","<manifestid>",NULL}, 0,
"Remove the manifest for the given manifest, payload or bundle from the Rhizome store"},
{app_rhizome_delete,{"rhizome","delete","file|","<fileid>",NULL}, 0,
"Remove the file with the given hash from the Rhizome store"},
{app_rhizome_direct_sync,{"rhizome","direct","sync","[<url>]",NULL}, 0,
"Synchronise with the specified Rhizome Direct server. Return when done."},
@ -2454,12 +2449,8 @@ struct cli_schema command_line_options[]={
"Create a new identity in the keyring protected by the provided PIN"},
{app_keyring_set_did,{"keyring", "set","did" KEYRING_PIN_OPTIONS,"<sid>","<did>","<name>",NULL}, 0,
"Set the DID for the specified SID. Optionally supply PIN to unlock the SID record in the keyring."},
{app_id_self,{"id","self",NULL}, 0,
"Return my own identity(s) as URIs"},
{app_id_self,{"id","peers",NULL}, 0,
"Return identity of known routable peers as URIs"},
{app_id_self,{"id","allpeers",NULL}, 0,
"Return identity of all known peers as URIs"},
{app_id_self,{"id","self|peers|allpeers",NULL}, 0,
"Return identity(s) as URIs of own node, or of known routable peers, or all known peers"},
{app_route_print, {"route","print",NULL}, 0,
"Print the routing table"},
{app_network_scan, {"scan","[<address>]",NULL}, 0,

View File

@ -565,8 +565,7 @@ int monitor_process_command(struct monitor_context *c)
int argc = parse_argv(c->line, ' ', argv, 16);
struct cli_parsed parsed;
int res = cli_parse(argc, (const char *const*)argv, monitor_commands, &parsed);
if (res == -1 || cli_invoke(&parsed, c))
if (cli_parse(argc, (const char *const*)argv, monitor_commands, &parsed) || cli_invoke(&parsed, c))
return monitor_write_error(c, "Invalid command");
return 0;
}

View File

@ -266,12 +266,22 @@ static void console_command(char *line){
int argc = parse_argv(line, ' ', argv, 16);
struct cli_parsed parsed;
int ret = cli_parse(argc, (const char *const*)argv, console_commands, &parsed);
if (ret == -1) {
switch (cli_parse(argc, (const char *const*)argv, console_commands, &parsed)) {
case 0:
cli_invoke(&parsed, NULL);
break;
case 1:
printf("Unknown command, try help\n");
fflush(stdout);
} else {
cli_invoke(&parsed, NULL);
break;
case 2:
printf("Ambiguous command, try help\n");
fflush(stdout);
break;
default:
printf("Error\n");
fflush(stdout);
break;
}
}