# HG changeset patch
# Parent 92fa48ef53c909928706ab4c51518953339a38e4
Unified command option parsing
diff -r 92fa48ef53c9 mcabber/mcabber/commands.c
--- a/mcabber/mcabber/commands.c Sun Jan 27 00:40:37 2013 +0200
+++ b/mcabber/mcabber/commands.c Sun Feb 24 06:49:31 2013 +0200
@@ -1634,130 +1634,109 @@
static void do_say_to(char *arg)
{
- char **paramlst;
- char *fjid, *msg_utf8;
+ char *fjid;
char *msg;
- char *unescaped_msg = NULL;
- char *uncompletedfjid = NULL;
- char *file = NULL;
+ char *file;
+ gchar *freeme = NULL; // fjid
+ gchar *freeme2 = NULL; // msg
LmMessageSubType msg_type = LM_MESSAGE_SUB_TYPE_NOT_SET;
- bool quiet = FALSE;
- bool eval = FALSE;
+ cmdopts_t options = {
+ (cmdopt_t[5]){
+ { CMDOPT_SWITCH, 'n', "normal", { .swc = 0 } },
+ { CMDOPT_SWITCH, 'h', "headline", { .swc = 0 } },
+ { CMDOPT_SWITCH, 'e', "escapes", { .swc = 0 } },
+ { CMDOPT_SWITCH, 'q', "quiet", { .swc = 0 } },
+ { CMDOPT_LAST, 'f', "file", { .opt = NULL } },
+ },
+ (cmdarg_t[2]){
+ { CMDOPT_REQUIRED, { .arg = NULL } },
+ { CMDOPT_LAST | CMDOPT_PLAIN | CMDOPT_CATCHALL, { .arg = NULL } },
+ },
+ NULL,
+ NULL,
+ };
if (!xmpp_is_online()) {
scr_LogPrint(LPRINT_NORMAL, "You are not connected.");
return;
}
- msg_type = scan_mtype(&arg);
- paramlst = split_arg(arg, 2, 1); // jid, message (or option, jid, message)
-
- if (!*paramlst) { // No parameter?
- scr_LogPrint(LPRINT_NORMAL, "Please specify a Jabber ID.");
- free_arg_lst(paramlst);
- return;
+ { // parse arguments
+ const char *error = cmdopts_parse(arg, &options);
+ if (error != NULL) {
+ scr_log_print(LPRINT_NORMAL, error);
+ return;
+ }
}
- // Check for an option parameter
- while (*paramlst) {
- if (!strcmp(*paramlst, "-q")) {
- char **oldparamlst = paramlst;
- paramlst = split_arg(*(oldparamlst+1), 2, 1); // jid, message
- free_arg_lst(oldparamlst);
- quiet = TRUE;
- } else if (!strcmp(*paramlst, "-e")) {
- char **oldparamlst = paramlst;
- paramlst = split_arg(*(oldparamlst+1), 2, 1); // jid, message
- free_arg_lst(oldparamlst);
- eval = TRUE;
- } else if (!strcmp(*paramlst, "-f")) {
- char **oldparamlst = paramlst;
- paramlst = split_arg(*(oldparamlst+1), 2, 1); // filename, jid
- free_arg_lst(oldparamlst);
- if (!*paramlst) {
- scr_LogPrint(LPRINT_NORMAL, "Wrong usage.");
- free_arg_lst(paramlst);
- return;
- }
- file = g_strdup(*paramlst);
- // One more parameter shift...
- oldparamlst = paramlst;
- paramlst = split_arg(*(oldparamlst+1), 2, 1); // jid, nothing
- free_arg_lst(oldparamlst);
- } else
- break;
+ if (options.opts[0].value.swc) {
+ msg_type = LM_MESSAGE_SUB_TYPE_NORMAL;
+ } else if (options.opts[1].value.swc) {
+ msg_type = LM_MESSAGE_SUB_TYPE_HEADLINE;
}
- if (!*paramlst) {
- scr_LogPrint(LPRINT_NORMAL, "Wrong usage.");
- free_arg_lst(paramlst);
- return;
- }
-
- fjid = *paramlst;
- msg = *(paramlst+1);
-
+ fjid = options.args[0].value.arg;
+ msg = options.args[1].value.arg;
+ file = options.opts[4].value.opt;
+
+ // ideally, this should go to commandline parsing subsystem
if (fjid[0] == '.') {
const gchar *cjid = (current_buddy ? CURRENT_JID : NULL);
if (fjid[1] == '\0') {
- fjid = g_strdup(cjid);
+ fjid = (char *)cjid; // FIXME
} else if (fjid[1] == JID_RESOURCE_SEPARATOR) {
char *res_utf8 = to_utf8(fjid+2);
- fjid = g_strdup_printf("%s%c%s", cjid, JID_RESOURCE_SEPARATOR, res_utf8);
+ freeme = fjid = g_strdup_printf("%s%c%s", cjid, JID_RESOURCE_SEPARATOR, res_utf8);
g_free(res_utf8);
- } else
- fjid = to_utf8(fjid);
- } else
- fjid = to_utf8(fjid);
-
+ }
+ }
+
+ // ditto
if (!strchr(fjid, JID_DOMAIN_SEPARATOR)) {
const gchar *append_server = settings_opt_get("default_server");
if (append_server) {
gchar *res = strchr(fjid, JID_RESOURCE_SEPARATOR);
- uncompletedfjid = fjid;
if (res) {
*res++ = '\0';
fjid = g_strdup_printf("%s%c%s%c%s", fjid, JID_DOMAIN_SEPARATOR, append_server,
JID_RESOURCE_SEPARATOR, res);
} else
fjid = g_strdup_printf("%s%c%s", fjid, JID_DOMAIN_SEPARATOR, append_server);
+ g_free(freeme);
+ freeme = fjid;
}
}
+ // as well
if (check_jid_syntax(fjid)) {
scr_LogPrint(LPRINT_NORMAL, "Please specify a valid Jabber ID.");
- free_arg_lst(paramlst);
- g_free(uncompletedfjid);
- g_free(fjid);
+ g_free(freeme);
return;
}
- if (!file) {
- msg_utf8 = to_utf8(msg);
- if (eval) {
- unescaped_msg = ut_unescape_tabs_cr(msg_utf8);
+ if (file == NULL) {
+ if (options.opts[2].value.swc) {
+ freeme2 = ut_unescape_tabs_cr(msg);
// We must not free() if the original string was returned
- if (unescaped_msg == msg_utf8)
- unescaped_msg = NULL;
+ if (freeme2 == msg)
+ freeme2 = NULL;
+ else
+ msg = freeme2;
}
- msg = (unescaped_msg ? unescaped_msg : msg_utf8);
} else {
char *filename_xp;
if (msg)
scr_LogPrint(LPRINT_NORMAL, "say_to: extra parameter ignored.");
filename_xp = expand_filename(file);
- msg = msg_utf8 = load_message_from_file(filename_xp);
+ freeme2 = msg = load_message_from_file(filename_xp);
g_free(filename_xp);
- g_free(file);
}
- send_message_to(fjid, msg, NULL, msg_type, quiet);
-
- g_free(uncompletedfjid);
- g_free(fjid);
- g_free(msg_utf8);
- g_free(unescaped_msg);
- free_arg_lst(paramlst);
+ send_message_to(fjid, msg, NULL, msg_type, options.opts[3].value.swc);
+
+ cmdopts_free(&options);
+ g_free(freeme);
+ g_free(freeme2);
}
// buffer_updown(updown, nblines)
diff -r 92fa48ef53c9 mcabber/mcabber/utils.c
--- a/mcabber/mcabber/utils.c Sun Jan 27 00:40:37 2013 +0200
+++ b/mcabber/mcabber/utils.c Sun Feb 24 06:49:31 2013 +0200
@@ -555,6 +555,312 @@
*str = tolower(*str);
}
+// FURTHER TODO:
+// Allow to specify catchall argument in the middle of string (requires some reverse parser)
+// Better error messages (caller frees them)
+// --help generates error with short usage, based on info in options struct
+
+// in_space -> in_space, in_optstart, in_argstart
+// in_optstart -> in_shortoptend, in_longoptstart, in_argstart ('-')
+// in_shortoptend -> in_space, error
+// in_longoptstart -> in_longopt, in_space, in_argstart ('---')
+// in_longopt -> in_longopt, in_space, error
+// in_argstart -> in_arg, success
+// in_arg -> in_arg, in_space, error
+
+// arguments: rw buffer in utf8, end of buffer pointer, options description struct
+static const char *cmdopts_parse_internal(gchar *arg, gchar *e, cmdopts_t *options)
+{
+ // parser state
+ enum {
+ in_space,
+ in_optstart,
+ in_shortoptstart,
+ in_shortoptend,
+ in_longoptstart,
+ in_longopt,
+ in_argstart,
+ in_arg,
+ } state = in_space;
+ // current pointer, start of object pointer
+ gchar *p, *s;
+ //
+ gboolean quotes = FALSE;
+ gboolean opts_ended = FALSE;
+ // option, for which argument is currently parsed
+ cmdopt_t *option = NULL;
+ // argument, that we currently parse
+ cmdarg_t *argument = NULL;
+ // flags of option/argument
+ guint flags = 0;
+ // error message to return
+ const char *error = NULL;
+
+ p = arg;
+ // we allow parser to do one extra run on final '\0'
+ while (p <= e && error == NULL) {
+ if (state == in_space) { // space between args/options
+ if (*p == ' ' || *p == '\0') { // still space
+ p ++;
+ } else if (*p == '-' && !opts_ended) { // option
+ state = in_optstart;
+ p ++;
+ } else { // argument
+ if (!option) {
+ opts_ended = TRUE;
+ }
+ s = p;
+ state = in_argstart;
+ }
+ } else if (state == in_optstart) { // long/short option
+ if (*p == ' ' || *p == '\0') { // argument '-'
+ opts_ended = TRUE;
+ s = p - 1;
+ state = in_argstart;
+ } else if (*p == '-') { // long option
+ state = in_longoptstart;
+ p ++;
+ } else { // short option
+ s = p;
+ state = in_shortoptend;
+ p ++;
+ }
+ } else if (state == in_shortoptend) { // short option
+ if (*p == ' ' || *p == '\0') { // option really ended
+ gboolean found = FALSE;
+ option = options -> opts;
+ if (option) {
+ do {
+ if (option -> shortopt == *s) {
+ found = TRUE;
+ break;
+ }
+ } while ((!(option++ -> flags & CMDOPT_LAST)) && !found);
+ }
+ if (found) { // option is known
+ if (option -> flags & CMDOPT_SWITCH) { // it is switch
+ if (option -> flags & CMDOPT_CATCHALL) {
+ option -> value.swc ++;
+ } else {
+ option -> value.swc = !option -> value.swc;
+ }
+ option = NULL;
+ } else { // it is option
+ if (*p == '\0') {
+ error = "Short option argument not specified";
+ }
+ }
+ state = in_space;
+ p ++;
+ } else { // option is unknown
+ error = "Unknown short option";
+ }
+ } else { // short option not ended
+ error = "Extra characters at short option end";
+ }
+ } else if (state == in_longoptstart) { // long option initialization
+ if (*p == ' ' || *p == '\0') { // end of options '--'
+ opts_ended = TRUE;
+ state = in_space;
+ p ++;
+ } else if (*p == '-') { // argument, starting with '---'
+ opts_ended = TRUE;
+ s = p - 2;
+ state = in_argstart;
+ } else { // it is long option
+ s = p;
+ state = in_longopt;
+ p ++;
+ }
+ } else if (state == in_longopt) { // long option name
+ if (*p == ' ' || *p == '\0') { // long option ended
+ gboolean found = FALSE;
+ gboolean eof = *p == '\0';
+ *p = '\0';
+ option = options -> opts;
+ if (option) {
+ do {
+ if (!g_strcmp0 (option -> longopt, s)) {
+ found = TRUE;
+ break;
+ }
+ } while ((!(option++ -> flags & CMDOPT_LAST)) && !found);
+ }
+ if (found) { // option is known
+ if (option -> flags & CMDOPT_SWITCH) { // it is switch
+ if (option -> flags & CMDOPT_CATCHALL) {
+ option -> value.swc ++;
+ } else {
+ option -> value.swc = !option -> value.swc;
+ }
+ option = NULL;
+ } else { // it is option
+ if (eof) {
+ error = "Long option argument not specified";
+ }
+ }
+ state = in_space;
+ p ++;
+ } else { // option is unknown
+ error = "Unknown long option";
+ }
+ } else { // still long option
+ p ++;
+ }
+ } else if (state == in_argstart) { // option/command argument initialization
+ if (option) {
+ flags = option -> flags & ~CMDOPT_CATCHALL; // catchall in options indicates multi-options
+ } else {
+ if (!argument) {
+ argument = options -> args;
+ }
+ if (!argument) { // no need to parse arguments at all
+ break;
+ }
+ flags = argument -> flags;
+ if ((flags & CMDOPT_CATCHALL) && (flags & CMDOPT_PLAIN)) { // can finish right away
+ argument -> value.arg = s;
+ break;
+ }
+ }
+ quotes = FALSE;
+ state = in_arg;
+ } else if (state == in_arg) { // option/command argument value
+ if (*p == '\0' && quotes) { // end of line in quotes
+ error = "Unfinished quoted argument";
+ } else if ((*p == ' ' && (!quotes) && !(flags & CMDOPT_CATCHALL)) || *p == '\0') { // argument ended
+ if (*p != '\0') {
+ *p = '\0';
+ p ++;
+ }
+ if (option) { // option argument
+ if (option -> flags & CMDOPT_CATCHALL) { // multi-value option
+ option -> value.multiopt = g_slist_append (option -> value.multiopt, s);
+ } else { // single-value option
+ option -> value.opt = s;
+ }
+ option = NULL;
+ } else { // command argument
+ if (argument -> flags & CMDOPT_SUBCOMMAND) {
+ gboolean found = FALSE;
+ subcmd_t *subcommand = options -> cmds;
+ if (subcommand) {
+ do {
+ if (!g_strcmp0(s, subcommand -> name)) {
+ found = TRUE;
+ break;
+ }
+ } while (!(subcommand++ -> flags & CMDOPT_LAST));
+ }
+ if (found) {
+ argument -> value.cmd = subcommand;
+ error = cmdopts_parse_internal(p, e, subcommand -> options);
+ break;
+ } else {
+ error = "Unknown subcommand";
+ }
+ } else {
+ argument -> value.arg = s;
+ if (argument -> flags & CMDOPT_LAST) { // last argument
+ break;
+ }
+ argument ++;
+ }
+ }
+ state = in_space;
+ } else if (*p == '\\' && !(flags & CMDOPT_PLAIN)) { // next char escape
+ memmove(p, p+1, e-(p+1));
+ e --;
+ if (*p == '\0') {
+ error = "Escape at the end of line";
+ }
+ p ++;
+ } else if (*p == '"' && !(flags & CMDOPT_PLAIN)) { // quotation start/end
+ memmove(p, p+1, e-(p+1));
+ e --;
+ quotes = !quotes;
+ } else { // still argument
+ p ++;
+ }
+ }
+ }
+
+ // check required flags on options
+ if (error == NULL && options -> opts) {
+ option = options -> opts;
+ do {
+ if (option -> flags & CMDOPT_REQUIRED) {
+ if (option -> flags & CMDOPT_SWITCH) {
+ // no way to check trigger switches, but no point in it as well
+ if (option -> flags & CMDOPT_CATCHALL && option -> value.swc == 0) {
+ error = "Required switch is not specified";
+ break;
+ }
+ } else {
+ if ((option -> flags & CMDOPT_CATCHALL && option -> value.multiopt == NULL) ||
+ ((!(option -> flags & CMDOPT_CATCHALL)) && option -> value.opt == NULL)) {
+ error = "Required option is not specified";
+ break;
+ }
+ }
+ }
+ } while (!(option++ -> flags & CMDOPT_LAST));
+ }
+
+ // check required flags on arguments
+ if (error == NULL && options -> args) {
+ argument = options -> args;
+ do {
+ if (argument -> flags & CMDOPT_REQUIRED) {
+ if (argument -> flags & CMDOPT_SUBCOMMAND) {
+ if (argument -> value.cmd == NULL) {
+ error = "Subcommand is not specified";
+ break;
+ }
+ } else {
+ if (argument -> value.arg == NULL) {
+ error = "Required argument is not specified";
+ break;
+ }
+ }
+ }
+ } while (!(argument++ -> flags & CMDOPT_LAST));
+ }
+
+ return error;
+}
+
+const char *cmdopts_parse(const char *arg, cmdopts_t *options)
+{
+ gchar *utf8 = to_utf8(arg);
+ gchar *e;
+
+ for (e = utf8; *e; e++);
+ options -> freeme = utf8;
+ return cmdopts_parse_internal(utf8, e, options);
+}
+
+void cmdopts_free(cmdopts_t *options)
+{
+ cmdopt_t *option = options -> opts;
+ subcmd_t *subcommand = options -> cmds;
+ if (option) {
+ do {
+ if ((option -> flags & (CMDOPT_CATCHALL|CMDOPT_SWITCH)) == CMDOPT_CATCHALL) {
+ g_slist_free(option -> value.multiopt);
+ option -> value.multiopt = NULL;
+ }
+ } while (!(option++ -> flags & CMDOPT_LAST));
+ }
+ if (subcommand) {
+ do {
+ cmdopts_free(subcommand -> options);
+ } while (!(subcommand++ -> flags & CMDOPT_LAST));
+ }
+ g_free(options -> freeme);
+ options -> freeme = NULL;
+}
+
// strip_arg_special_chars(string)
// Remove quotes and backslashes before an escaped quote
// Only quotes need a backslash
diff -r 92fa48ef53c9 mcabber/mcabber/utils.h
--- a/mcabber/mcabber/utils.h Sun Jan 27 00:40:37 2013 +0200
+++ b/mcabber/mcabber/utils.h Sun Feb 24 06:49:31 2013 +0200
@@ -43,6 +43,93 @@
char **split_arg(const char *arg, unsigned int n, int dontstriplast);
void free_arg_lst(char **arglst);
+// error cmdopts_parse (argstring, optionlist)
+// Function parses command argument string according to provided list of
+// options and arguments. If in this process it encounters an error, it
+// returns error string (that should be displayed and g_free'd afterwards).
+// Note: For now returned error is constant string, that shouldn't be freed,
+// but we're getting there.
+// After processing you should free freeme and any GSList values of catchall
+// options (only lists itself, not values). For your convenience, there is
+// cmdopts_free(), that does exactly that.
+// The function recognizes four kinds of expressions:
+// - Options with arguments in a form '-f bar' or '--foo bar'
+// - Switches without arguments in a form '-f' or '--foo'
+// - End-of-options marker '--'
+// - Individual arguments ('-' and '---' are considered arguments too)
+// To define command line syntax, you pass cmdopts_t struct, that contains
+// two contiguous lists of cmdopt_t and cmdarg_t structs accordingly. The
+// last struct in list must have CMDOPT_LAST flag set.
+// You can specify your own default values, they will be replaced/appended
+// if needed.
+// You can omit specifying longopt or shortopt (put NULL or '\0' there).
+// Note: returned values and arguments are already converted to utf8.
+
+// Flags:
+// Only applies to options, defined if option does not have argument.
+#define CMDOPT_SWITCH ( 1<<0 )
+// Don't process quotes and escapes in argument (applies to option arguments too).
+#define CMDOPT_PLAIN ( 1<<1 )
+// For options - put all encountered values into GSList value.multiopt
+// instead of overwriting value.opt.
+// For switches - increment value.swc instead of logical flipping.
+// For arguments - grab the rest of the line without splitting on spaces.
+// Implicitly last argument.
+#define CMDOPT_CATCHALL ( 1<<2 )
+// Option/argument must have value.
+#define CMDOPT_REQUIRED ( 1<<3 )
+// Last entry in struct sequence.
+#define CMDOPT_LAST ( 1<<4 )
+// Argument only, argument is the name for subcommand.
+// Implicitly last argument.
+#define CMDOPT_SUBCOMMAND ( 1<<5 )
+
+// thoughts about future:
+// command struct contains cmdopts
+// cmdopt/cmdarg struct contains argument type, that implies completion id and argument correctness checks
+// cmdopt/cmdarg struct contains default value
+// when building completion for command, we allow options (if not before --)
+// would be good to have 'subcommands' mcabber commands
+//
+// so, the process of command execution looks like:
+// - we walk through the options, set default values
+// - we parse argument string, populating options
+// - we check for required options availability
+// - we call callback
+// - we free resources
+typedef struct cmdopts_struct cmdopts_t;
+typedef struct {
+ guint flags;
+ const char *name;
+ cmdopts_t *options;
+} subcmd_t;
+typedef struct {
+ guint flags;
+ char shortopt;
+ const char *longopt;
+ union {
+ GSList *multiopt;
+ gchar *opt;
+ guint swc;
+ } value;
+} cmdopt_t;
+typedef struct {
+ guint flags;
+ union {
+ gchar *arg;
+ subcmd_t *cmd;
+ } value;
+} cmdarg_t;
+struct cmdopts_struct {
+ cmdopt_t *opts;
+ cmdarg_t *args;
+ subcmd_t *cmds;
+ gchar *freeme;
+};
+
+const char *cmdopts_parse (const char *arg, cmdopts_t *options);
+void cmdopts_free(cmdopts_t *options);
+
void replace_nl_with_dots(char *bufstr);
char *ut_expand_tabs(const char *text);
char *ut_unescape_tabs_cr(const char *text);