# HG changeset patch
# Parent 1b0b563a81e6425da07c1da7ea4f947c4f3326cb
[work-in-progress] Unified command option parsing

  * complete change of commands.h interface
    - uses different symbols to make compatibility layer possible
  * completion system is broken now
  * usable commands:
    - roster
    - color
    - status/status_to
    - add/del
    - group
    - say/msay/say_to
    - buffer
    - clear
    - info
  * it does compile, but have not tested at all
  * privatized say_cmd()
  * dropped cmd_setstatus()
  * process_line() still expects line to be in local encoding,
    while cmd_execute() expects utf8 and rw
  * process_line() can return error values, different from 255

  ** PREVIOUS ACHIEVEMENTS **

  * function interface changes:
    * buddy_search() now expects argument in utf8
    * cmd_setstatus() now expects separate status and message arguments
    * say_cmd()'s second argument is now of new type msgtype_t
    * scr_multi* now store multiline in utf8
  * /del:
    * allows specifying jid, as /add does
    * -n(--dryrun) switch for debugging purposes
  * /rename:
    * -r(--reset) instead of '-'
    * -j(--jid), -g(--group), -n(--name)
  * /move:
    * -j(--jid), -n(--name)
  * misc:
    * fix help for /buffer date

diff -r 1b0b563a81e6 mcabber/doc/HOWTO_commands.mdwn
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/HOWTO_commands.mdwn	Wed May 15 12:48:30 2013 +0300
@@ -0,0 +1,977 @@
+
+**New commands interface for MCabber**
+
+[[!toc levels=2]]
+
+# Overview
+
+New command interface was designed with next goals in mind:
+
+ * Share as much argument checking code as possible.
+ * Remove cumbersome parsing code from commands.
+ * Encourage adding options and switches to commands.
+ * Use the same rules, when handling arguments everywhere.
+ * Integrate and improve completion system.
+ * Add common generic stuff, like '--help'.
+ * Try to still be lightweight.
+ * Try to still be readable.
+
+It is built around static structure, "command description".  User can add or
+remove these structures to list of commands.  *FIXME* more
+
+## Command description struct, 'cmdopts_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef struct cmdopts_struct cmdopts_t;
+
+typedef enum {
+    cmd_default = 0x0000,
+    cmd_safe    = 0x0001,
+} cmd_flags_t;
+
+struct cmdopts_struct {
+    const char          *name;
+    const cmd_flags_t   flags;
+    const cmd_checker_t check;
+    const cmd_handler_t handle;
+    const cmdopt_t      *opts;
+    const cmdarg_t      *args;
+    const cmdopts_t     *cmds;
+    const gpointer      userdata;
+    size_t              valno;
+};
+
+// --------------------------------------------------------------     """     ]]
+
+This struct describes command as a whole and links to argument descriptions.
+This struct is also used to describe individual subcommands, as they are quite
+similar to normal command, because they can have their own options, arguments
+and subcommands.  The fields of this struct:
+
+ * 'name' - name of the command or subcommand.
+
+ * 'flags' - currently there's only one flag:
+
+    + 'cmd_safe' -  command is safe to execute in main mcabberrc during
+      initialization.  Have no meaning for subcommands.
+    + 'cmd_default' - default value of no flags enabled.
+ 
+ * 'check' - execution environment checker for command.  This callback is used
+   to do general checks before even parsing command line.  You can write your
+   own checker or use standard ones, for example - 'cmd_check_online', that
+   checks, if you are currently online.
+
+ * 'handle' - command function.  It is executed only if command line was
+   successfully parsed and argument values passed the type checking.  Unused in
+   subcommands.
+
+ * 'opts' - pointer to the array of 'cmdopt_t' structs, describing command-line
+   options ("-f bar") and switches ("-x"), that this command accepts.  Array
+   must end with option, that have 0 as short option character.
+ 
+ * 'args' - similarly, pointer to the array of 'cmdarg_t' structs, that describe
+   command-line positional arguments (in order).  Array must end with argument,
+   that have NULL name.
+ 
+ * 'cmds' - pointer to the array of subcommands of this command (or subcommand).
+   How parser switches to subcommands we will describe later.  Array must end
+   with subcommand, that have NULL name.
+ 
+ * 'userdata' - arbitrary pointer, where you can put some data, that should
+   accompany this command or subcommand.  Unused by parser.
+
+ * 'valno' - this is internal value, that is initialized at command definition
+   time, you should not modify it.  Currently unused in subcommands.
+
+## Command function, 'cmd_handler_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef gchar *(*cmd_handler_t) (const cmdopts_t *command,
+                                 cmdarg_value_t  *values);
+
+// --------------------------------------------------------------     """     ]]
+
+Command function is passed it's command definition struct (mainly to give it
+access to userdata) and dynamically allocated array of parsed argument values.
+
+It should return NULL in case all went smoothly, or dynamically allocated error
+string, that will be g_free()'d after displaying.
+
+So, as you can see, command definition should give parser info on what can
+appear in command line and how to map all these options and arguments to array
+of values.
+
+## Option description struct, 'cmdopt_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef struct {
+    const char stortopt;
+    const char *longopt;
+    cmdarg_t   arg;
+} cmdopt_t;
+
+// --------------------------------------------------------------     """     ]]
+
+This struct just adds short option character and long option name to generic
+argument struct, that we'll look at right now.
+
+## Argument description struct, 'cmdarg_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef struct cmdarg_struct cmdarg_t;
+
+typedef enum {
+    cmdarg_default  = 0x0000,
+    cmdarg_catchall = 0x0001,
+    cmdarg_plain    = 0x0002,
+    cmdarg_check    = 0x0004,
+    cmdarg_required = 0x0008,
+    cmdarg_subcmd   = 0x0010,
+    cmdarg_switch   = 0x0020,
+    cmdarg_eol      = 0x0003, // catchall + plain
+    cmdarg_chreq    = 0x000C, // check + required
+    cmdarg_special  = 0x0030, // subcmd + switch
+    cmdarg_trigger  = 0x0024, // switch + check
+} cmdarg_flags_t;
+
+struct cmdarg_struct {
+    const char           *name;
+    const guint          pos;
+    const cmdarg_flags_t flags;
+    const char           *defval;
+    const cmdarg_type    *type;
+    gconstpointer        chkdata;
+    gconstpointer        userdata;
+};
+
+// --------------------------------------------------------------     """     ]]
+
+This struct stores information about mapping between command-line entity
+(switch, option, argument, subcommand) and element in 'values' array.  First,
+let's briefly describe fields, and then walk through their use in different
+entities.
+
+Fields:
+
+ * 'name' - argument name, mainly used for help (not implemented yet) and error
+   messages.
+ 
+ * 'pos' - this is the index in 'values' array, where argument value should be
+   stored.
+ 
+ * 'flags' - various tweaks to parsing process:
+
+    + 'catchall' - argument value will catch everything, remaining on command
+      line, even if unescaped spaces will appear in it.
+    + 'plain' - do not treat backslashes ('\') and quotes ('"') as escaping
+      characters.
+    + 'check' - call argument value checker (defined in 'type' field), even if
+      argument was not assigned the value during the parsing process.
+    + 'required' - if mentioned checker will return error, with this flag set,
+      this error will be considered fatal, and command function will not be
+      called.
+    + 'subcmd' - this argument is subcommand.
+    + 'switch' - this argument is part of option definition, and this option
+      have no argument (but it still needs to store switch state somewhere).
+    + 'eol' - shortcut for "rest of command line without modification".
+    + 'chreq' - shortcut for "argument must have a valid value".
+    + 'special' - this is special type of argument - subcommand or switch.
+ 
+ * 'defval' - default value for argument in "unchecked" form - i.e., in the form
+   of string, as it appears on command line (we'll discuss type checkers and
+   what can they do to value later).  Before parsing command line, parser will
+   assign this value to corresponding value structure.
+
+ * 'type' - pointer to structure, describing argument type.
+
+ * 'chkdata' - if type needs some additional info, it is a place to supply it.
+
+ * 'userdata' - place for arbitrary info, that should accompany this argument or
+   option.  Unused by parser.
+
+Now, let's discuss general case - option with argument or positional argument.
+
+When parser encounters such argument, it checks 'catchall' and 'plain' flags and
+crops argument value from command line accordingly, then it assigns it to the
+'value.arg' field of 'cmdarg_value_t' struct in array 'values' with index, given
+in 'pos'.  Also it marks such value as 'visited' and puts a link to argument
+description into value's 'src' field.  More about effect of these actions later,
+in 'cmdarg_value_t' section.
+
+However, if argument struct is a part of option description struct, and have
+flag 'switch' set, parser will not try to parse value.  Instead it will increase
+the count in 'value.swc' in corresponding 'cmdarg_value_t' struct.  Argument
+name for switches is essentially ignored, but for safety sake it is recommended
+to duplicate switch long option name here.  Flags 'catchall' and 'plain'
+obviously have no effect.  Flag 'check' makes switch a trigger, that flips
+between 'on' and 'off' for every occurence of flag on command line.  Flags
+'required' and 'subcmd' also have no effect for obvious reasons.  Default value
+is ignored for switches, they always start with count 0 (off).  Since switch is
+internal type, argument type field is ignored as well.  Flags and source fields
+of value are updated as usual.
+
+If flag 'subcmd' is set for positional argument, parser crops argument value
+according to flags as usual, but instead of assigning it, walks through list of
+subcommands in command description struct and compares obtained value to
+subcommand names.  If it finds corresponding subcommand, it assigns pointer to
+subcommand description struct to 'value.cmd' field of corresponding
+'cmdarg_value_t' struct and updates it's source and flags.  Then, instead of
+proceeding with parsing process, it recursively calls parser on remaining part
+of command line, passing subparser subcommand description.  Note, that if
+subcommand parser will end parsing before hitting end of command line, parser
+will proceed with parsing arguments for main command.  Default value and
+argument type fields are ignored for subcommands.
+
+Now let's take a look at value structure, that you'll be dealing with the most
+in the actual code.
+
+## Argument value structure, 'cmdarg_value_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef struct cmdarg_value_struct cmdarg_value_t;
+
+typedef enum {
+    cmdval_default = 0x0000,
+    cmdval_visited = 0x0100,
+    cmdval_freeme  = 0x0200,
+} cmdval_flags_t;
+
+struct cmdarg_value_struct {
+    const cmdarg_t *src;
+    cmdval_flags_t flags;
+    union {
+        guint           uint;
+        gint            sint;
+        guint           swc;
+        const gchar     *roarg;
+        gchar           *arg;
+        const cmdopts_t *cmd;
+        struct {
+            gpointer    bud;
+            gchar       *resource;
+        } rjid;
+        gpointer        ptr;
+    } value;
+};
+
+// --------------------------------------------------------------     """     ]]
+
+Command may happen to be called recursively - i.e., something in command may
+cause the event, that will cause another call to the same command, thus we
+cannot store actual values in command/argument definition struct, and have to
+allocate dynamic memory for them.  This struct is designed exactly for this.
+Let's take a look at it's fields:
+
+ * 'src' - this points to the argument description, from which this struct is
+   holding value right now (note, that value can be initialized several times
+   during parsing process from different arguments).
+ 
+ * 'flags' - to hold parser and typechecker marks:
+
+    + 'visited' - parser uses this to track values, initialized from command
+      line as opposed to values, holding default value.
+    + 'freeme' - used by argument type checker to tell parser, that it needs to
+      call value destructor callback on value.
+ 
+ * 'value' - union, that contains various possible forms of value:
+
+    + 'uint'  - generic unsigned integer.
+    + 'sint'  - generic signed integer.
+    + 'swc'   - switch occurence count.
+    + 'roarg' - XXX read-only string - default value or like.
+    + 'arg'   - generic string value (read-write).
+    + 'cmd'   - pointer to subcommand description struct.
+    + 'rjid'  - roster jid value - pair of roster buddy and resource string.
+    + 'ptr'   - used for anything, that does not fits into these types.
+
+To better understand, how these fields are used, let's walk through parsing
+process.
+
+## Parsing
+
+Parser starts by allocating memory for values array, and then initializing it by
+walking through command description, looking at options and arguments and
+assigning default values to corresponding entries in array.  It also puts
+pointer to the argument description into value's 'src' field.  Thus, all used in
+command description values will have this field initialized, even if they were
+not specified on command line.  This comes handly later, when checking for
+reqired value presence.  For switches parser just sets the counter to zero.
+Note, that parser does not descend into subcommands at this stage.  It does the
+same procedure for subcommand, but later, when it already knows which subcommand
+is selected.  Also note, that if several arguments have the same value index,
+parser will use latest encountered one to initialize the value.  This is used
+for default value in "argument clustering", that I'll show you later.
+
+Then parser calls command environment checker callback (if present), and if it
+returns error - terminates the process right now.  Note, that subcommands can
+also have checkers.
+
+Next parser does its job of parsing command line.  Each time it extracts
+argument value, it into 'value.arg' field of corresponding value entry and
+pointer to argument description struct into 'src' field.  Also it sets 'visited'
+flag on value.  At this stage value is still just unchecked string, except for
+special argument types.  For switch occurence count in 'value.swc' gets
+increased each time argument was specified.  Note however, that if several
+switches use the same value index ("clustered switches"), counter gets reset,
+when switches change one another in sequence - i.e. "-e -s -s -s" will count as
+three "-s", but "-s -s -e -s" will count as only one "-s".  For subcommands
+parser checks for corresponding subcommand in 'cmds' list, assigns it to
+'value.cmd' and recursively passes the end of command line to be parsed with
+subcommand description.  Note, that for subcommands parser does "checking on the
+spot" - if parsed argument value does not match any subcommand, and argument
+have 'required' flag set, it raises error immediately (if flag is not set, it
+merely assigns NULL and proceeds parsing according to current command
+description).
+
+Then parser walks through array of values and performs value checking.  Note,
+that all values, that need checking at this point should have 'src' field
+initialized - either set at default value assignment step, or during parsing,
+so, parser knows properties of value's argument.  Parser only pays attention to
+the values, that either have 'visited' flag set (i.e. provided by user) or that
+have 'check' flag in argument description (useful for mandatory arguments or
+default values, that need convesion).  If value corresponds to a switch, and
+argument have 'check' flag set, switch occurence count is replaced by remainder
+of division it by 2 (this way switch behaves like trigger).  If it is a
+subcommand, and it have 'required' flag set, parser checks, if it have non-NULL
+value.  If it is usual argument (option or positional), and it does have 'type',
+that have 'check' callback set, parser calls this checker, passing it value
+structure (again, value structure contains pointer to argument description, so,
+checker can access 'chkdata' field, supplied by user).  If checker returns error
+string and argument have 'required' flag set, parser raises error.  If flag is
+not set, parser just prints warning and proceeds with checking.
+
+If checking was successful, parser calls command function, providing it with
+command description and values array.  This function can also return error, but
+at this stage it does not change process, only causes error message to be
+printed.
+
+And finally parser frees allocated resources - walks through values array,
+calling argument type 'free' callback on values, that have 'freeme' flag set and
+then frees the array itself.
+
+## Argument type, 'cmdarg_type_t'
+
+[[!format c """// --------------------------------------------------------------
+
+typedef struct cmdarg_type_struct cmdarg_type_t;
+
+typedef gchar *(*cmdarg_checker_t) (cmdarg_value_t *value);
+typedef void (*cmdarg_destructor_t) (cmdarg_value_t *value);
+// FIXME: this one is still not designed
+typedef void (*cmdarg_completor_t) (void);
+
+struct cmdarg_type_struct {
+    cmdarg_checker_t    check;
+    cmdarg_destructor_t free;
+    cmdarg_completor_t  complete;
+};
+
+// --------------------------------------------------------------     """     ]]
+
+As you can see, argument type is nothing more than a set of callbacks:
+
+ * 'check' - check parsed argument value for conformance to type rules, possibly
+   replace with something different, like corresponding integer value or roster
+   buddy pointer.
+
+ * 'free' - if type checker may need to free some data afterwards, this callback
+   should be set to corresponding function, and each time checker really needs
+   value to be freed, it should set flag 'freeme' on value.
+
+ * 'complete' - *FIXME* not yet designed callback, that will return list of
+   possible completions according to given prefix.
+
+After parsing command line parser performs argument value checking, that's where
+it calls 'check' callbacks.   Checker is given pointer to value structure, that
+it needs to check.  Checker can modify value string (except when it is default
+value, but you have to supply your default values so, that they do not need
+modifying) or completely replace it with another string or even non-string
+object.  If checker uses some resources (eg.  allocates memory for replacement
+value), it can set the flag 'freeme' on value to request call to value
+destructor, when values array will be freed.  If checker needs some additional
+data (eg. it is some generic checker, that needs list of valid values or other
+parameters), these data can be supplied in 'chkdata' field.  Checker function
+should return NULL on success or error string, that will be g_free()'d by
+parser.  Take note, that if argument does not have 'reqired' flag set, parser
+will ignore checker error, so, it is recommended to nullify invalid value before
+returning error (but it is not required).
+
+# Examples
+
+When writing description for a command, first thing, you have to do - is to
+determine, which values your command can get from user.  You don't have to be
+shy - new interface is designed to encourage commands to be as flexible and
+option-rich as possible.  
+
+Second - consider, which ones are to be specified as positional arguments, and
+which should become options or switches.
+
+Next you will want to decide, which checks and restrictions should you put on
+values.  Essentially, determine value type.
+
+And then you can begin writing command definition.  So, let's start with
+something simple.
+
+## Single-argument no-checks command
+
+Let's look at command like /say, that have only one argument, that should be
+passed as is, and with no restrictions:
+
+    /ex1 message...
+
+Definition for such command will look like:
+
+[[!format c """// --------------------------------------------------------------
+
+// command function predeclaration
+gchar *do_ex1 (const cmdopts_t *command, cmdarg_value_t *values);
+
+// command arguments definition
+cmdopts_t def_ex1 = {
+    "ex1",              // command name
+    cmd_default,        // flags
+    NULL,               // checker
+    do_ex1,             // command function
+    NULL,               // list of options - none
+    (cmdarg_t[2]){      // list of arguments
+        {
+            "message",  // argument name
+            0,          // value index
+            // flags:
+            // - plain:    do not interpret quotes and escapes
+            // - catchall: do not end argument on unescaped spaces
+            cmdarg_plain | cmdarg_catchall,
+            NULL,       // default value
+            NULL,       // argument type
+        },
+        {NULL}          // this is an argument list end marker
+    },
+    NULL,               // list of subcommands - none
+};
+
+// Command function gets shown above command description (we don't need it) and
+// argument value list.  Returns error message or NULL.
+gchar *do_ex1 (const cmdopts_t *command, cmdarg_value_t *values)
+{
+    gchar *message = values[0].value.arg;
+    // now do something with message:
+    // - check, if message was given at all
+    if (!message || !*message)
+        // return error, it will be printed, prepended by command name
+        return g_strdup("You need to specify a message!");
+    // - use the value
+    scr_log_print (LPRINT_NORMAL, "Got the message: \"%s\".", message);
+    // no error occured
+    return NULL;
+}
+
+...
+// register our command
+cmd_define (&def_ex1);
+...
+// remove command
+cmd_undef (&def_ex1);
+...
+
+// --------------------------------------------------------------     """     ]]
+
+A lot of things to do to achieve a simple goal - does not look quite appealing
+so far.  Still, let's tweak our example a bit.
+
+Remember the third step - decide, which checks should apply to our argument.
+Now, look at our command - we check, if message is NULL or if message is empty.
+But imagine, that user has given us a message " " - it's of no big use to us,
+so, probably, we should also strip leading/trailing spaces before doing the
+check.  That's where argument types come into play.  We can write argument
+checker for that! But luckily, we already have built-in standard checker, that
+does exactly what we need - checks if string contains non-space characters.  All
+we need to do - to specify '&cmdarg_type_nonspace' as argument type and remove
+our check inside of the command:
+
+[[!format c """// --------------------------------------------------------------
+
+...
+cmdopts_t def_ex1 = {
+    "ex1",
+    cmd_default,
+    NULL,
+    do_ex1,
+    NULL,
+    (cmdarg_t[2]){
+        {
+            "message",
+            0,
+            // flags:
+            // - plain: do not interpret quotes and escapes
+            // - catchall: do not end argument on unescaped spaces
+            // - check: always invoke checker, even if user omitted the argument
+            // - required: terminate processing, if checker returns error
+            // a lot of flags, but we can use shortcuts:
+            // - eol = plain + catchall: get everything to the end of line as is
+            // - chreq = check + required: argument needs to have a valid value
+            cmdarg_eol | cmdarg_chreq,
+            NULL,
+            // strip spaces, check if result have non-zero length
+            &cmdarg_type_nonspace,
+        },
+        {NULL}
+    },
+    NULL,
+};
+
+gchar *do_ex1 (const cmdopts_t *command, cmdarg_value_t *values)
+{
+    scr_log_print (LPRINT_NORMAL, "Got the message: \"%s\".",
+                   values[0].value.arg);
+    return NULL;
+}
+...
+
+// --------------------------------------------------------------     """     ]]
+
+Ok, that's a little bit better.  Now let's move on to something more complex.
+
+## Switches
+
+Let's add switches '-s' and '-l', that will define, where to print the message
+to - to log or to screen.  For that we will need another two value indices - one
+for each switch.
+
+[[!format c """// --------------------------------------------------------------
+
+...
+cmdopts_t def_ex1 = {
+    "ex1",
+    cmd_default,
+    NULL,
+    do_ex1,
+    (cmdopt_t[3]){
+        {                    // first switch: [-s|--screen]
+            's',             // short option name
+            "screen",        // long option name
+            {
+              "screen",      // argument name - unused
+              1,             // value position
+              // flags:
+              // - switch: this is switch
+              cmdarg_switch,
+              NULL,          // default value - unused
+              NULL,          // type - unused
+            }
+        },
+                             // second switch: [-l|--log]
+        { 'l', "log", { "log", 2, cmdarg_switch, NULL, NULL } },
+        {0}                  // end of list marker
+    },
+    (cmdarg_t[2]){
+        { "message", 0, cmdarg_eol | cmdarg_chreq, NULL,
+                                                   &cmdarg_type_nonspace },
+        {NULL}
+    },
+    NULL,
+};
+
+gchar *do_ex1 (const cmdopts_t *command, cmdarg_value_t *values)
+{
+    // default value
+    guint whereto = LPRINT_NORMAL;
+    // -s is default, so, check, that it is not set before checking for -l
+    if (!values[1].value.swc)
+        if (values[2].value.swc)
+            whereto = LPRINT_LOG;
+    scr_log_print (whereto, "Got the message: \"%s\".", values[0].value.arg);
+    return NULL;
+}
+...
+
+// --------------------------------------------------------------     """     ]]
+
+Ok, that works, but what if user have aliases, and wants last specified option
+to override the value? Currently, if -s was once specified, -l will not have any
+effect, regardless of count or position in command line.  Not that good.  Let's
+use the trick, that I call "argument clustering".  We'll specify the same value
+index for both switches.  Since 'value' struct have the pointer to the argument,
+it was initialized from last time, we can recognize, which switch was used last.
+By default this pointer points to the last argument with this index in command
+definition - we can use that to specify default value.  Now, to identify
+switches we can use argument names, but 'argument' struct contains userdata
+field, where we can put our LPRINT_* constants and just use it directly.  So,
+with clustered switches, we will have:
+
+[[!format c """// --------------------------------------------------------------
+
+...
+cmdopts_t def_ex1 = {
+    "ex1",
+    cmd_default,
+    NULL,
+    do_ex1,
+    (cmdopt_t[3]){
+        // Set both argument indices to 1, specify our constants in userdata
+        // field.  Screen is default value, thus, it goes last.
+        { 'l', "log",    { "log",    1, cmdarg_switch, NULL, NULL, NULL,
+                             (gpointer)LPRINT_LOG    } },
+        { 's', "screen", { "screen", 1, cmdarg_switch, NULL, NULL, NULL,
+                             (gpointer)LPRINT_NORMAL } },
+        {0}
+    },
+    (cmdarg_t[2]){
+        { "message", 0, cmdarg_eol | cmdarg_chreq, NULL,
+                                                   &cmdarg_type_nonspace },
+        {NULL}
+    },
+    NULL,
+};
+
+gchar *do_ex1 (const cmdopts_t *command, cmdarg_value_t *values)
+{
+    scr_log_print ((guint)values[1].src -> userdata,
+                   "Got the message: \"%s\".", values[0].value.arg);
+    return NULL;
+}
+...
+
+// --------------------------------------------------------------     """     ]]
+
+That's much better.  This trick may be quite useful not only with switches, but
+also with options, sometimes even clustering options with arguments can be
+handy.
+
+## Options
+
+Options are not much different from normal arguments, except there you'll see
+'check' and 'required' mostly only in combination with default value - otherwise
+it defeats the purpose - to be optional.
+
+[[!format c """// --------------------------------------------------------------
+
+// TODO:
+//   example (not really used as of now - were too complex to deal using old
+//   interface).
+
+// --------------------------------------------------------------     """     ]]
+
+## Subcommands
+
+Now, let's discuss another internal argument type - subcommand.  Since
+subcommands are quite common in mcabber, and since they have quite big impact on
+parsing process, they were made a part of parser.
+
+Currently, only positional arguments can be subcommands.  You can have options or
+other arguments precede them, though in practice there's no examples of that so
+far.
+
+So, to indicate, that argument is a subcommand, you just add flag 'subcmd'.
+When parser will encounter such argument, it will look up command structure with
+specified name in the list 'cmds' of command definition and proceed parsing,
+using that command definition instead of main one.  A good example of command
+with several completely different subcommands would be '/color', so, let's look:
+
+[[!format c """// --------------------------------------------------------------
+
+static gchar *do_color (const cmdopts_t *command, cmdarg_value_t *values);
+
+// We will put these values in subcommand definition 'userdata' fields
+// to simplify the task of determining, which subcommand was actually selected.
+typedef enum {
+    scmd_color_roster,
+    scmd_color_mucnick,
+    scmd_color_muc,
+} scmd_color_t;
+
+// We define value inedxes as enum to make value access expressions
+// self-explanatory.
+typedef enum {
+    pos_color_scmd          = 0,
+    pos_color_roster_status = 1,
+    pos_color_roster_jid    = 2,
+    pos_color_roster_color  = 3,
+    pos_color_muc_room      = 1,
+    pos_color_muc_mode      = 2,
+    pos_color_nick_nick     = 1,
+    pos_color_nick_color    = 2,
+} pos_color_t;
+
+// This is a helper struct for cmdarg_type_string2enum checker.
+// The checker will compare value to strings and replace value.arg
+// with corresponding value.uint.
+static const string2enum_t s2e_color_muc[] = {
+    { "on",     MC_ALL    },
+    { "off",    MC_OFF    },
+    { "preset", MC_PRESET },
+    { "-",      MC_REMOVE },
+    { NULL,     0         },
+};
+
+static cmdopts_t def_color = {
+    "color",
+    // This command is allowed in main config file during initialization, thus
+    // it have the 'safe' flag.
+    cmd_safe,
+    NULL,
+    do_color,
+    // no options
+    NULL,
+    // only one argument - subcommand
+    (cmdarg_t[2]){{ "subcommand", pos_color_scmd, cmdarg_subcmd | cmdarg_chreq,
+                                                           NULL, NULL },{NULL}},
+    // three subcommands
+    (cmdopts_t[4]){
+        // First subcommand - 'roster' with three arguments.
+        {"roster", cmd_default, NULL, NULL, NULL, (cmdarg_t[4]){
+              { "statusmask|clear", pos_color_roster_status, cmdarg_chreq, NULL,
+                           &cmdarg_type_color_statusmask, (gpointer)"ofdna_?" },
+              { "jidmask",          pos_color_roster_jid,    cmdarg_check, NULL,
+                           &cmdarg_type_bjidmask },
+              { "color|-",          pos_color_roster_color,  cmdarg_check, NULL,
+                           &cmdarg_type_color    },
+              {NULL}
+            },
+            // Subcommand can have its own subcommands, but in this case we
+            // don't have them.
+            NULL,
+            // We use userdata to determine, which subcommand was selected.
+            (gpointer)scmd_color_roster},
+        // Second subcommand - 'muc' with two arguments.
+        {"muc", cmd_default, NULL, NULL, NULL, (cmdarg_t[3]){
+              { "roomjid",         pos_color_muc_room, cmdarg_chreq, NULL,
+                           &cmdarg_type_color_roomjid },
+              { "on|off|preset|-", pos_color_muc_mode, cmdarg_chreq, "on",
+                           &cmdarg_type_string2enum, (gpointer)s2e_color_muc },
+              {NULL}
+            }, NULL, (gpointer)scmd_color_muc},
+        // Third subcommand - 'mucnick' also with two arguments.
+        {"mucnick", cmd_default, NULL, NULL, NULL, (cmdarg_t[3]){
+              { "nick",    pos_color_nick_nick,  cmdarg_chreq, NULL,
+                           &cmdarg_type_nick  },
+              { "color|-", pos_color_nick_color, cmdarg_chreq, NULL,
+                           &cmdarg_type_color },
+              {NULL}
+            }, NULL, (gpointer)scmd_color_mucnick},
+        {NULL}
+    },
+};
+
+static gchar *do_color (const cmdopts_t *options, cmdarg_value_t *values)
+{
+    scmd_color_t subcmd =
+                  (scmd_color_t) (values[pos_color_scmd].value.cmd -> userdata);
+
+    if (subcmd == scmd_color_roster) {
+        const gchar *status   = values[pos_color_roster_status].value.arg;
+        const gchar *wildcard = values[pos_color_roster_jid].value.arg;
+        const gchar *color    = values[pos_color_roster_color].value.arg;
+        if (!strcmp(status, "clear")) { // Not a color command, clear all
+            scr_roster_clear_color();
+            update_roster = TRUE;
+        } else {
+            // Unfortunately, due to "clear" case not taking any arguments,
+            // we cannot check for argument presence using 'required' flag.
+            if ((!wildcard) || (!color)) {
+              return g_strdup ("Missing argument.");
+            } else {
+              update_roster = scr_roster_color(status, wildcard, color) ||
+                              update_roster;
+            }
+        }
+    } else if (subcmd == scmd_color_muc) {
+        scr_muc_color ( values[pos_color_muc_room].value.arg,
+                        values[pos_color_muc_mode].value.uint );
+    } else { // scmd_color_mucnick
+        scr_muc_nick_color( values[pos_color_nick_nick].value.arg,
+                            values[pos_color_nick_color].value.arg );
+    }
+
+    return NULL;
+}
+
+// --------------------------------------------------------------     """     ]]
+
+Here you also see a lot of new types:
+
+ * 'color_statusmask' - specific to "/color" command wrapper over generic type
+   'charset'.  This type allows only word "clear" or string, composed of
+   specified in 'chkdata' field characters, in this case tese are, of course,
+   "ofdna_?".
+
+ * 'bjidmask' - this type only provides completion, otherwise it is the same as
+   'nonspace'.
+
+ * 'color' - checks value to be a valid name of mcabber color.
+
+ * 'color_roomjid' - specific to "/color" command wrapper over type 'bjid'.
+   Type allows the string "*" or a valid bare jid.
+
+ * 'string2enum' - generic type, that converts string into unsigned integer
+   value according to given in 'chkdata' dictionary of name-value pairs.
+
+ * 'nick' - as 'bjidmask' - only provides completion.
+
+Note, that although here I used value indexes 1 and 2 several times for
+different arguments, it is not "clustering", as they are used in different
+subcommands.
+
+## Argument types
+
+Let's take a look at simple checker, that we've encountered first - 'nonspace':
+
+[[!format c """// --------------------------------------------------------------
+
+// Checker gets parsed value string in 'value.arg', argument description in
+// 'src' and returns error string or NULL.
+gchar *cmdarg_check_nonspace (cmdarg_value_t *arg)
+{
+    // current value
+    gchar *val = arg -> value.arg;
+
+    // was value given at all?
+    if (val) {
+        // skip leading spaces
+        while (isspace (*val))
+            val ++;
+        if (*val) { // valid arg - string contains non-space symbols
+            // reassing value in case of stripped leading space
+            arg -> value.arg = val;
+            // strip trailing space
+            while (*val)
+                val ++;
+            while (isspace (*val))
+                val --;
+            val ++;
+            // Note: this needs write access, so, default value cannot contain
+            // trailing spaces!
+            if (*val)
+                *val = '\0';
+            // no error, argument is valid
+            return NULL;
+        }
+    }
+
+    // Returned error may be ignored by parser, if 'required' flag is not set on
+    // argument, so, we nullify argument to ensure, that invalid value will not
+    // be passed to command.
+    arg -> value.arg = NULL;
+    return g_strdup ("Non-space value required.");
+}
+
+// type definition
+const cmdarg_type_t cmdarg_type_nonspace = {
+    // our checker
+    cmdarg_check_nonspace,
+    // freeing callabck - no need for that
+    NULL,
+    // completion callabck - none, we can't know, what string may contain
+    NULL,
+};
+
+// --------------------------------------------------------------     """     ]]
+
+Quite simple, I hope.  Now, let's look at more complex type - 'fjid':
+
+[[!format c """// --------------------------------------------------------------
+
+// This checker checks syntax of fjid and expands "current-buddy" expressions
+// "." and "./resource".
+gchar *cmdarg_check_fjid (cmdarg_value_t *arg)
+{
+    gchar *error = NULL;
+
+    // We're using nonspace checker to check our value - empty string is not a
+    // valid jid.
+    if (!(error = cmdarg_check_nonspace(arg))) {
+        // Now, we're sure, that we have non-space string
+        const char *fjid = arg -> value.arg;
+
+        // We check for "current-buddy" expression.
+        if (fjid[0] == '.' &&
+                       (fjid[1] == JID_RESOURCE_SEPARATOR || fjid[1] == '\0')) {
+            const char *jid;
+            if (!current_buddy) {
+              error = g_strdup_printf ("No buddy selected.");
+            } else if (!(jid = buddy_getjid(BUDDATA(current_buddy)))) {
+              error = g_strdup_printf ("Current buddy have no jid.");
+            } else if (fjid[1] == '\0') {
+              arg -> value.roarg = jid;
+            } else {
+              // We allocate value - we will need to free it, so, we as well set
+              // 'freeme' flag.
+              arg -> value.arg = g_strdup_printf ("%s%c%s",
+                                         jid, JID_RESOURCE_SEPARATOR, fjid + 2);
+              arg -> flags    |= cmdval_freeme;
+            }
+        // this is jid - check, that it is valid
+        } else if (check_jid_syntax(fjid)) {
+            error = g_strdup_printf ("Jid <%s> is invalid.", fjid);
+        }
+    }
+
+    // As before, nullify value in case of error
+    if (error)
+        arg -> value.arg = NULL;
+    return error;
+}
+
+// Free callback will be called only if freeme flag is set, so, we can
+// just g_free() value without any checks.
+void cmdarg_free_gfree (cmdarg_value_t *arg)
+{
+    g_free (arg -> value.arg);
+}
+
+const cmdarg_type_t cmdarg_type_fjid = {
+    // checker
+    cmdarg_check_fjid,
+    // freer
+    cmdarg_free_gfree,
+    // completor, while possible, is not implemented, as whole completion system is
+    // not yet designed.
+    NULL,
+};
+
+// --------------------------------------------------------------     """     ]]
+
+If possible, you are encouraged to re-use existing checkers - for example, bjid
+checker uses fjid checker to expand "current-buddy" expressions and check
+syntax, and only strips resource afterwards:
+
+[[!format c """// --------------------------------------------------------------
+
+gchar *cmdarg_check_bjid (cmdarg_value_t *arg)
+{
+    gchar *error = NULL;
+
+    if (!(error = cmdarg_check_fjid(arg))) {
+        gchar *res = strchr (arg -> value.arg, JID_RESOURCE_SEPARATOR);
+        if (res != NULL)
+            *res = '\0';
+    }
+
+    // Error can only happen inside fjid callback, that will nullify argument
+    // for us.
+    return error;
+}
+
+const cmdarg_type_t cmdarg_type_bjid = {
+    cmdarg_check_bjid,
+    // may need to free value - we're using fjid checker internally
+    cmdarg_free_gfree,
+    NULL,
+};
+
+// --------------------------------------------------------------     """     ]]
+
+So far we've only modified string in value.  But checkers are not limited to
+this, for example, uint checker performs atoi() on value and assigns resulting
+number to value.uint.  Take a look at definition of cmdarg_value_t struct -
+value is actually a union of different types of value.  If you need something
+different from existing - you can always allocate your own struct and use
+value.ptr.  However, if you think, that your case is generic enough - contact
+mcabber developers, we'll consider adding more variants there.  Maybe we'll even
+add your argument type to built-in types.
+
+<!-- vim: se ts=4 sw=4 et filetype=markdown tw=80: -->
diff -r 1b0b563a81e6 mcabber/doc/help/cs/hlp_buffer.txt
--- a/mcabber/doc/help/cs/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/cs/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Přesune se o [n] řádků nahoru (výchozí: polovina obrazovky).
 /buffer down [n]
  Přesune se o [n] řádků dolů (výchozí: polovina obrazovky).
-/buffer date [datum]
+/buffer date datum
  Přesune se na první řádek po datu [datum] (formát: "RRRR-mm-dd").
 /buffer % n
  Přesune se na procentuální pozici n%.
diff -r 1b0b563a81e6 mcabber/doc/help/cs/hlp_del.txt
--- a/mcabber/doc/help/cs/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/cs/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Smaže aktuální kontakt ze seznamu kontaktů (rosteru) a zruší povolení oznamování o stavu daného kontaktu (autorizaci) na obou stranách.
diff -r 1b0b563a81e6 mcabber/doc/help/cs/hlp_move.txt
--- a/mcabber/doc/help/cs/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/cs/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [skupina]
+ /MOVE [-j|--jid jid] [-n|--name name] [skupina]
 
 Přesune aktuální kontakt do požadované skupiny. Není-li skupina zadána, přesune se kontakt do výchozí skupiny. Pokud skupina neexistuje, automaticky se založí.
+You can select other buddy that current using options --jid and --name.
 Tip: V módu rozhovoru lze použít "/roster alternate" pro skok na přesunutý kontakt.
diff -r 1b0b563a81e6 mcabber/doc/help/cs/hlp_rename.txt
--- a/mcabber/doc/help/cs/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/cs/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME jméno
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] jméno
 
-Přejmenuje aktuálního uživatele nebo skupinu na 'jméno'. Je-li 'jméno' znak '-', dané jméno se odstraní ze seznamu kontaktů (rosteru) a zobrazí se 'jid'.
+Přejmenuje aktuálního uživatele nebo skupinu na 'jméno'.
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/de/hlp_buffer.txt
--- a/mcabber/doc/help/de/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/de/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Scrollt den Puffer um n Zeilen hoch. Gibt man keine Zahl an, scrollt er um einen halben Bildschirm
 /buffer down [n]
  Scrollt den Puffer um n Zeilen runter. Gibt man keine Zahl an, scrollt er um einen halben Bildschirm
-/buffer date [date]
+/buffer date date
  Springe zu der ersten Zeile nach dem Datum, welches im Format "JJJJ-mm-tt" anstatt [date] angegeben werden muss
 /buffer % n
  Springe zur Position "n" im Chatpuffer
diff -r 1b0b563a81e6 mcabber/doc/help/de/hlp_del.txt
--- a/mcabber/doc/help/de/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/de/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Löscht den gerade ausgewählten Buddy vom Roster. Außerdem werden die automatischen Presence Benachrichtigungen vom/zum Buddy gestoppt.
diff -r 1b0b563a81e6 mcabber/doc/help/de/hlp_move.txt
--- a/mcabber/doc/help/de/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/de/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,6 +1,7 @@
 
- /MOVE [groupname]
+ /MOVE [-j|--jid jid] [-n|--name name] [groupname]
 
 Verschiebt den ausgewählten Buddy zu der Gruppe "groupname". Wenn keine Gruppe angegeben wurde, wird der Buddy zur Standardgruppe verschoben. Wenn die Gruppe "groupname" nicht existiert, wird sie erst erschaffen.
+You can select other buddy that current using options --jid and --name.
 
 Tipp: Wenn der Chatmodus aktiviert ist, kannst du "/roster alternate" benutzen um zu dem gerade bewegten Buddy zu springen.
diff -r 1b0b563a81e6 mcabber/doc/help/de/hlp_rename.txt
--- a/mcabber/doc/help/de/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/de/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME name
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] newname
 
-Setzt den Namen des gerade ausgewählten Buddys bzw. der ausgewählten Gruppe auf "name". Wenn "name" - ist, so wird der Name vom Roster entfernt (mcabber wird in diesem Falle die JID bzw. den username anzeigen)
+Setzt den Namen des gerade ausgewählten Buddys bzw. der ausgewählten Gruppe auf "name".
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/en/hlp_buffer.txt
--- a/mcabber/doc/help/en/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/en/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Scroll the buffer up [n] lines (default: half a screen)
 /buffer down [n]
  Scroll the buffer down [n] lines (default: half a screen)
-/buffer date [date]
+/buffer date date
  Jump to the first line after the specified [date] in the chat buffer (date format: "YYYY-mm-dd")
 /buffer % n
  Jump to position %n of the buddy chat buffer
diff -r 1b0b563a81e6 mcabber/doc/help/en/hlp_del.txt
--- a/mcabber/doc/help/en/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/en/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
-Delete the current buddy from our roster, unsubscribe from its presence notification and unsubscribe it from ours.
+Delete the current buddy or one, specified with [jid] from our roster, unsubscribe from its presence notification and unsubscribe it from ours.
diff -r 1b0b563a81e6 mcabber/doc/help/en/hlp_move.txt
--- a/mcabber/doc/help/en/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/en/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [groupname]
+ /MOVE [-j|--jid jid] [-n|--name name] [groupname]
 
 Move the current buddy to the requested group.  If no group is specified, then the buddy is moved to the default group.  If the group "groupname" doesn't exist, it is created.
+You can select other buddy that current using options --jid and --name.
 Tip: if the chatmode is enabled, you can use "/roster alternate" to jump to the moved buddy.
diff -r 1b0b563a81e6 mcabber/doc/help/en/hlp_rename.txt
--- a/mcabber/doc/help/en/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/en/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME name
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] newname
 
-Rename the current buddy or group to the given "name".  If "name" is -, the name is removed from the roster (and mcabber will display the JID or username).
+Rename the current buddy or group to the given "newname".
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/fr/hlp_buffer.txt
--- a/mcabber/doc/help/fr/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/fr/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Défile vers le haut de [n] lignes (par défaut un demi écran)
 /buffer down [n]
  Défile vers le bas de [n] lignes (par défaut un demi écran)
-/buffer date [date]
+/buffer date date
  Va à la première ligne après la [date] dans le tampon actuel (format: "aaaa-mm-jj")
 /buffer % n
  Va à la position n% du tampon
diff -r 1b0b563a81e6 mcabber/doc/help/fr/hlp_del.txt
--- a/mcabber/doc/help/fr/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/fr/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Supprime le contact sélectionné du roster, supprime notre abonnement à ses notifications de présence et supprime son abonnement aux nôtres.
diff -r 1b0b563a81e6 mcabber/doc/help/fr/hlp_move.txt
--- a/mcabber/doc/help/fr/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/fr/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [groupname]
+ /MOVE [-j|--jid jid] [-n|--name name] [groupname]
 
 Déplace le contact sélectionné vers le groupe spécifié. Si aucun groupe n'est donné, le contact est déplacé vers le groupe par défaut. Si le groupe "groupname" n'existe pas, il est créé.
+You can select other buddy that current using options --jid and --name.
 Astuce : si le mode discussion (chatmode) est activé, vous pouvez utiliser "/roster alternate" pour vous positionner sur le contact que vous venez de déplacer.
diff -r 1b0b563a81e6 mcabber/doc/help/fr/hlp_rename.txt
--- a/mcabber/doc/help/fr/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/fr/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME nom
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] nom
 
-Renomme le contact/groupe sélectionné avec le "nom" spécifié. Si le nom est «-», le nom est supprimé du roster (et mcabber affichera le JID ou le nom d'utilisateur).
+Renomme le contact/groupe sélectionné avec le "nom" spécifié.
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/it/hlp_buffer.txt
--- a/mcabber/doc/help/it/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/it/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Fa scorrere indietro il buffer di [n] linee (default: metà schermo)
 /buffer down [n]
  Fa scorrere avanti il buffer di [n] linee (default: metà schermo)
-/buffer date [data]
+/buffer date data
  Salta alla prima linea successiva alla [data] specificata nel buffer di chat (formato della data: "YYYY-mm-dd")
 /buffer % n
  Salta alla posizione %n del buffer di chat corrente
diff -r 1b0b563a81e6 mcabber/doc/help/it/hlp_del.txt
--- a/mcabber/doc/help/it/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/it/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Elimina il contatto corrente dal roster, cancellando la sottoscrizione alle reciproche notifiche della propria presenza.
diff -r 1b0b563a81e6 mcabber/doc/help/it/hlp_move.txt
--- a/mcabber/doc/help/it/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/it/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [gruppo]
+ /MOVE [-j|--jid jid] [-n|--name name] [grouppo]
 
 Muove il contatto corrente nel gruppo richiesto. Se non viene specificato alcun gruppo, il contatto viene spostato nel gruppo si default. Se il gruppo "gruppo" non esiste, viene creato.
+You can select other buddy that current using options --jid and --name.
 Trucco: se la modalità chat è abilitata, puoi usare "/roster alternate" per spostarti sul contatto appena mosso.
diff -r 1b0b563a81e6 mcabber/doc/help/it/hlp_rename.txt
--- a/mcabber/doc/help/it/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/it/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME nome
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] nome
 
-Rinomina il contatto od il gruppo correnti usando "nome". Se si specifica - come "nome", il nome viene rimosso dal roster, ed al suo posto verranno visulizzati il JID o lo username.
+Rinomina il contatto od il gruppo correnti usando "nome".
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/nl/hlp_buffer.txt
--- a/mcabber/doc/help/nl/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/nl/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Scroll de buffer [n] regels omhoog (standaard: een half scherm)
 /buffer down [n]
  Scroll de buffer [n] regels omlaag (standaard: een half scherm)
-/buffer date [datum]
+/buffer date datum
  Spring naar de eerste regel na de aangeduide [datum] in de chat buffer (datum formaat: "YYYY-mm-dd")
 /buffer % n
  Spring naar positie %n in de buddy chat buffer
diff -r 1b0b563a81e6 mcabber/doc/help/nl/hlp_del.txt
--- a/mcabber/doc/help/nl/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/nl/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Verwijder de actieve buddy uit ons roster, en zet het wederzijds toezenden van status veranderingen stop.
diff -r 1b0b563a81e6 mcabber/doc/help/nl/hlp_move.txt
--- a/mcabber/doc/help/nl/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/nl/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [groepsnaam]
+ /MOVE [-j|--jid jid] [-n|--name name] [groepsnaam]
 
 Verplaats de actieve buddy naar de aangegeven groep.  Indien geen groep werd gespecificeerd wordt buddy verplaatst naar de standaard groep.  Indien de groep "groepsnaam" niet bestaat, wordt die eerst aangemaakt.
+You can select other buddy that current using options --jid and --name.
 Tip: indien chatmode actief is, kun je "/roster alternate" gebruiken om direct naar de verplaatste buddy te springen.
diff -r 1b0b563a81e6 mcabber/doc/help/nl/hlp_rename.txt
--- a/mcabber/doc/help/nl/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/nl/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME naam
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] naam
 
-Hernoem de actieve buddy of groep in de aangegeven "naam". Indien '-' wordt gebruikt als 'naam', wordt die uit het roster verwijderd (en mcabber zal vervolgens de JID of gebruikersnaam tonen).
+Hernoem de actieve buddy of groep in de aangegeven "naam".
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/pl/hlp_del.txt
--- a/mcabber/doc/help/pl/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/pl/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
 Usuwa aktualnie zaznaczoną osobę z rostera, usuwa subskrypcję powiadomienia dostępności u danej osoby oraz u nas.
diff -r 1b0b563a81e6 mcabber/doc/help/pl/hlp_move.txt
--- a/mcabber/doc/help/pl/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/pl/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [nazwa grupy]
+ /MOVE [-j|--jid jid] [-n|--name name] [nazwa grupy]
 
 Przenosi aktualną osobę do grupy "nazwa grupy".  Jeśli nie podano nazwy grupy, wtedy osoba jest przenoszona do grupy domyślnej.  Jeśli grupa "nazwa grupy" nie istnieje, zostaje utworzona.
+You can select other buddy that current using options --jid and --name.
 Podpowiedź: jeśli jest włączony tryb czatu, możesz użyć "/roster alternate" aby skoczyć do przeniesionej osoby.
diff -r 1b0b563a81e6 mcabber/doc/help/pl/hlp_rename.txt
--- a/mcabber/doc/help/pl/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/pl/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME nazwa
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] nazwa
 
-Zmienia nazwę aktualnej osoby lub grupy na "nazwa". Jeżeli jako argument polecenia zostanie podany "-" wcześniejsza nazwa zostanie usunięta z listy kontaktów (mcabber będzie wyświetlał JID lub nazwę użytkownika). 
+Zmienia nazwę aktualnej osoby lub grupy na "nazwa". 
+If --reset is specified, "newname" is ignored and name will be reset to default - jid or username.
+Optionally you can use one of --jid, --group or --name to select object, different from current.
diff -r 1b0b563a81e6 mcabber/doc/help/ru/hlp_buffer.txt
--- a/mcabber/doc/help/ru/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/ru/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Перемещает на [n] строк вверх в буфере (истории переписки) (по умолчанию: половина экрана)
 /buffer down [n]
  Перемещает на [n] строк вниз в буфере (истории переписки) (по умолчанию: половина экрана)
-/buffer date [date]
+/buffer date date
  Перемещает в первой строке после определенной даты [date] в буфере (истории переписки) (формат даты: "год-месяц-день", для примера "2006-01-01")
 /buffer % n
  Перемещает на позицию %n в текущем буфере (истории переписки)
diff -r 1b0b563a81e6 mcabber/doc/help/ru/hlp_del.txt
--- a/mcabber/doc/help/ru/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/ru/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
-Удаляет текущего пользователя из списка контактов, отключает уведомления о его статусе и отключает уведомления пользователя о вашем статусе.
+Удаляет текущего пользователя (или указанного с помощью jid) из списка контактов, отключает уведомления о его статусе и отключает уведомление пользователя о вашем статусе.
diff -r 1b0b563a81e6 mcabber/doc/help/ru/hlp_move.txt
--- a/mcabber/doc/help/ru/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/ru/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,6 +1,7 @@
 
- /MOVE [groupname]
+ /MOVE [-j|--jid jid] [-n|--name name] [groupname]
 
 Перемещает пользователя в определенную группу. Если группа не указана, пользователь перещается в группу по умолчанию (default group). Если группа "groupname" не создана, она автоматически создается.
+С помощью параметров --jid и --name можно перемещать контакты, отличные от текущего.
 Полезно: Если включен режим чата (chatmode), Вы можете использовать "/roster alternate" для перехода к перемещенному пользователю.
 
diff -r 1b0b563a81e6 mcabber/doc/help/ru/hlp_rename.txt
--- a/mcabber/doc/help/ru/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/ru/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME name
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group name] [-n|--name name] newname
 
- Переименовывает текущего пользователя или группу в заданное имя "name". Если заданное имя имеет значение "-", то заданное ранее имя текущего пользователя или группы удаляется из списка контактов (mcabber будет отображать JID или имя пользователя по умолчанию).
+Переименовывает текущего пользователя или группу в заданное имя "newname".
+Если указан параметр --reset, "newname" игнорируется, а имя сбрасывается (mcabber будет отображать JID или имя пользователя по умолчанию).
+Для указания обьекта, отличного от текущего, можно использовать опции --jid, --group и --name.
diff -r 1b0b563a81e6 mcabber/doc/help/uk/hlp_buffer.txt
--- a/mcabber/doc/help/uk/hlp_buffer.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/uk/hlp_buffer.txt	Wed May 15 12:48:30 2013 +0300
@@ -25,7 +25,7 @@
  Посунути буфер вверх на n рядків (якщо не вказано - пів екрану).
 /buffer down [n]
  Посунути буфер вниз на n рядків (якщо не вказано - пів екрану).
-/buffer date [дата]
+/buffer date дата
  Перейти до першого повідомлення після дати (дата вигляду РРРР-ММ-ДД).
 /buffer % процент
  Перейти до вказаної у процентах позиції.
diff -r 1b0b563a81e6 mcabber/doc/help/uk/hlp_del.txt
--- a/mcabber/doc/help/uk/hlp_del.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/uk/hlp_del.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,4 @@
 
- /DEL
+ /DEL [-n|--dryrun] [jid]
 
-Потерти поточний контакт зі списку. На додачу відписатися від його повідомлень про статус і відписати його від ваших.
+Потерти поточний контакт (або контакт, що має вказаний jid) зі списку. Також відписатися від його сповіщень про статус і відписати його від ваших.
diff -r 1b0b563a81e6 mcabber/doc/help/uk/hlp_move.txt
--- a/mcabber/doc/help/uk/hlp_move.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/uk/hlp_move.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,5 +1,6 @@
 
- /MOVE [група]
+ /MOVE [-j|--jid jid] [-n|--name ім’я] [група]
 
-Переносить поточний контакт до вказаної групи. Якщо групу не вказати контакт опиниться у головній групі. Якщо група не існує, її буде створено.
+Переносить поточний контакт до вказаної групи. Якщо групу не вказати контакт опиниться у головній групі. Якщо групи не існує, її буде створено.
+За допомогою опцій --jid та --name можна перемістити контакт, відмінний від поточного.
 Примітка: в режимі розмови можна використати "/roster alternate", щоб перейти до нового місця контакту контакту.
diff -r 1b0b563a81e6 mcabber/doc/help/uk/hlp_rename.txt
--- a/mcabber/doc/help/uk/hlp_rename.txt	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/doc/help/uk/hlp_rename.txt	Wed May 15 12:48:30 2013 +0300
@@ -1,4 +1,6 @@
 
- /RENAME ім'я
+ /RENAME [-r|--reset] [-j|--jid jid] [-g|--group ім’я] [-n|--name ім’я] нове ім’я
 
 Змінює прізвисько поточного контакту або назву групи.
+За допомогою параметра --reset можна повернути контакту типову назву. При цьому нове ім’я (якщо вказане) ігнорується.
+Опції --jid, --group та --name дозволяють перейменовувати об’єкти, відмінні від поточного.
diff -r 1b0b563a81e6 mcabber/mcabber/commands.c
--- a/mcabber/mcabber/commands.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/commands.c	Wed May 15 12:48:30 2013 +0300
@@ -19,7 +19,7 @@
  * USA
  */
 
-#include <string.h>
+#include <string.h> // g_memmove
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -43,512 +43,680 @@
 #include "xmpp.h"
 #include "main.h"
 
-#define IMSTATUS_AWAY           "away"
-#define IMSTATUS_ONLINE         "online"
-#define IMSTATUS_OFFLINE        "offline"
-#define IMSTATUS_FREE4CHAT      "free"
-#define IMSTATUS_AVAILABLE      "avail"
-#define IMSTATUS_NOTAVAILABLE   "notavail"
-#define IMSTATUS_DONOTDISTURB   "dnd"
-#ifdef WITH_DEPRECATED_STATUS_INVISIBLE
-# define IMSTATUS_INVISIBLE      "invisible"
+//
+//  Pre-declarations
+//
+
+// for process_line()
+typedef enum {
+  scmd_group_unfold = 0,
+  scmd_group_fold   = 1,
+  scmd_group_toggle = -1,
+} scmd_group_t;
+typedef enum {
+  msgtype_not_set,
+  msgtype_normal,
+  msgtype_headline,
+} msgtype_t;
+
+static void group_cmd (gpointer group, scmd_group_t action);
+static void say_cmd (char *arg, msgtype_t msgtype);
+static void room_bookmark (gpointer bud, char *arg);
+
+#define BUILTIN_COUNT 18
+static cmdopts_t def_roster,     // 1
+                 def_color,
+                 def_status,
+                 def_status_to,
+                 def_add,        // 5
+                 def_del,
+                 def_group,
+                 def_say,
+                 def_msay,
+                 def_say_to,     // 10
+                 def_buffer,
+                 def_clear,
+                 def_info,
+                 def_rename,
+                 def_move;       // 15
+                 def_set,
+                 def_alias,
+                 def_bind,
+#if 0
+                 def_connect,
+                 def_disconnect,
+                 def_rawxml,
+                 def_room,
+                 def_authorization,
+                 def_version,
+                 def_request,
+                 def_event,
+                 def_help,
+                 def_pgp,
+                 def_iline,
+                 def_screen_refresh,
+                 def_chat_disable,
+                 def_source,
+                 def_otr,
+                 def_otrpolicy,
+                 def_echo,
+                 def_module,
+                 def_exit;
 #endif
 
-// Return value container for the following functions
-static int retval_for_cmds;
-
-// Commands callbacks
-static void do_roster(char *arg);
-static void do_status(char *arg);
-static void do_status_to(char *arg);
-static void do_add(char *arg);
-static void do_del(char *arg);
-static void do_group(char *arg);
-static void do_say(char *arg);
-static void do_msay(char *arg);
-static void do_say_to(char *arg);
-static void do_buffer(char *arg);
-static void do_clear(char *arg);
-static void do_info(char *arg);
-static void do_rename(char *arg);
-static void do_move(char *arg);
-static void do_set(char *arg);
-static void do_alias(char *arg);
-static void do_bind(char *arg);
-static void do_connect(char *arg);
-static void do_disconnect(char *arg);
-static void do_rawxml(char *arg);
-static void do_room(char *arg);
-static void do_authorization(char *arg);
-static void do_version(char *arg);
-static void do_request(char *arg);
-static void do_event(char *arg);
-static void do_help(char *arg);
-static void do_pgp(char *arg);
-static void do_iline(char *arg);
-static void do_screen_refresh(char *arg);
-static void do_chat_disable(char *arg);
-static void do_source(char *arg);
-static void do_color(char *arg);
-static void do_otr(char *arg);
-static void do_otrpolicy(char *arg);
-static void do_echo(char *arg);
-static void do_module(char *arg);
-static void do_exit(char *arg);
-
-static void room_bookmark(gpointer bud, char *arg);
-
-// Global variable for the commands list
-static GSList *Commands;
-static GSList *safe_commands;
-
-#ifdef MODULES_ENABLE
-#include "modules.h"
-
-gpointer cmd_del(gpointer id)
+//
+//  Main commands mechanics
+//
+// Functions to parse mcabber command line and execute commands accordingly.
+// Allows to define commands and remove definitions.
+
+cmdopts_t **cmd_list  = NULL; // command structs list
+gsize       cmd_count = 0;    // number of commands (one less than memory allocated)
+
+// private
+void cmd_init (void)
 {
-  GSList *sl_cmd;
-  if (!id) return NULL;
-  for (sl_cmd = Commands; sl_cmd; sl_cmd = sl_cmd->next)
-    if (sl_cmd -> data == id) {
-      cmd *command = (cmd *) sl_cmd->data;
-      gpointer userdata = command->userdata;
-      Commands = g_slist_delete_link(Commands, sl_cmd);
-      compl_del_category_word(COMPL_CMD, command->name);
-      g_slice_free(cmd, command);
-      return userdata;
+  cmd_list = g_new (cmdopts_t *, BUILTIN_COUNT+1);
+
+  cmd_list[0]  = &def_roster;
+  cmd_list[1]  = &def_color;
+  cmd_list[2]  = &def_status;
+  cmd_list[3]  = &def_status_to;
+  cmd_list[4]  = &def_add;
+  cmd_list[5]  = &def_del;
+  cmd_list[6]  = &def_group;
+  cmd_list[7]  = &def_say;
+  cmd_list[8]  = &def_msay;
+  cmd_list[9]  = &def_say_to;
+  cmd_list[10] = &def_buffer;
+  cmd_list[11] = &def_clear;
+  cmd_list[12] = &def_info;
+  cmd_list[13] = &def_rename;
+  cmd_list[14] = &def_move;
+  cmd_list[15] = &def_set;
+  cmd_list[16] = &def_alias;
+  cmd_list[17] = &def_bind;
+#if 0
+  cmd_list[18] = &def_connect;
+  cmd_list[19] = &def_disconnect;
+  cmd_list[20] = &def_rawxml;
+  cmd_list[21] = &def_room;
+  cmd_list[22] = &def_authorization;
+  cmd_list[23] = &def_version;
+  cmd_list[24] = &def_request;
+  cmd_list[25] = &def_event;
+  cmd_list[26] = &def_help;
+  cmd_list[27] = &def_pgp;
+  cmd_list[28] = &def_iline;
+  cmd_list[29] = &def_screen_refresh;
+  cmd_list[30] = &def_chat_disable;
+  cmd_list[31] = &def_source;
+  cmd_list[32] = &def_otr;
+  cmd_list[33] = &def_otrpolicy;
+  cmd_list[34] = &def_echo;
+  cmd_list[35] = &def_module;
+  cmd_list[36] = &def_exit;
+#endif
+  cmd_list[BUILTIN_COUNT] = NULL;
+
+  cmd_count = BUILTIN_COUNT;
+}
+
+// private
+void cmd_uninit (void)
+{
+  g_free (cmd_list);
+  cmd_list  = NULL;
+  cmd_count = 0;
+}
+
+static size_t cmdopts_count_values (const cmdopts_t *command)
+{
+  size_t max = -1;
+  if (command -> opts) {
+    const cmdopt_t *opt;
+    for (opt = command -> opts; opt -> shortopt != 0; opt ++)
+      if (opt -> arg.pos > max)
+        max = opt -> arg.pos;
+  }
+  if (command -> args) {
+    const cmdarg_t *arg;
+    for (arg = command -> args; arg -> name != NULL; arg ++)
+      if (arg -> pos > max)
+        max = arg -> pos;
+  }
+  if (command -> cmds) {
+    const cmdopts_t *scmd;
+    for (scmd = command -> cmds; scmd -> name != NULL; scmd ++) {
+      size_t cnt = cmdopts_count_values (scmd);
+      if (cnt > max)
+        max = cnt;
     }
+  }
+  return max;
+}
+
+void cmd_define (cmdopts_t *command)
+{
+  size_t n;
+  // we update value count even for existing commands - user may
+  // have screwed something up in struct.
+  command -> valno = cmdopts_count_values (command) + 1;
+  // check if command already exists
+  for (n = cmd_count; n > 0; n --)
+    if (cmd_list[n-1] == command) {
+      // raise it's priority
+      g_memmove(cmd_list+n-1, cmd_list+n, sizeof(cmdopts_t *) * (cmd_count-n-1));
+      cmd_list[cmd_count-1] = command;
+      return;
+    }
+  // register new command
+  cmd_list = g_renew (cmdopts_t *, cmd_list, cmd_count+2);
+  cmd_list[cmd_count]   = command;
+  cmd_list[cmd_count+1] = NULL;
+  cmd_count ++;
+}
+
+void cmd_undef (cmdopts_t *command)
+{
+  size_t num = 0;
+  while (num < cmd_count) {
+    if (cmd_list[num] == command) {
+      g_memmove (cmd_list+num, cmd_list+num+1, sizeof(cmdopts_t *) * (cmd_count-num));
+      cmd_list = g_renew (cmdopts_t *, cmd_list, cmd_count);
+      cmd_count --;
+      return;
+    }
+    num ++;
+  }
+}
+
+//  error cmdopts_parse_argument ( startptr, endptr, flags )
+// Parses next argument according to flags and finishes it with zero.
+// Updates current/end pointers. Parsed string MUST be writable.
+// String may shrink in size (quotation/escapes), thus endpointer is also
+// updated.
+const char *cmdopts_parse_argument(gchar **pr, gchar **er, cmdarg_flags_t flags)
+{
+  gchar *p = *pr;
+  gchar *e = *er;
+  const char *error = NULL;
+  gboolean   quotes = FALSE;
+
+  if ((flags & cmdarg_catchall) && (flags & cmdarg_plain)) {
+    *pr = e;
+    return NULL;
+  }
+
+  while (p <= e && error != NULL) {
+    if (p == e && quotes) { // end of line in quotes
+      error = "Unfinished quoted argument.";
+    } else if ((*p == ' ' && (!quotes) && !(flags & cmdarg_catchall)) || p == e) { // argument ended
+      if (*p != '\0') {
+        *p = '\0';
+        p ++;
+      }
+      break;
+    } else if (*p == '\\' && !(flags & cmdarg_plain)) { // next char escape
+      g_memmove(p, p+1, e-(p+1)+1);
+      e --;
+      if (p == e) {
+        error = "Escape at the end of line.";
+      } else {
+        p ++;
+      }
+    } else if (*p == '"' && !(flags & cmdarg_plain)) { // quotation start/end
+      g_memmove(p, p+1, e-(p+1)+1);
+      e --;
+      quotes = !quotes;
+    } else { // still argument
+      p ++;
+    }
+  }
+
+  *pr = p;
+  *er = e;
+  return error;
+}
+
+static cmdarg_value_t *cmdopts_allocate_values (const cmdopts_t *command)
+{
+  cmdarg_value_t *values;
+  size_t         n = command -> valno;
+
+  if (n == 0)
+    return NULL;
+
+  values = g_slice_alloc (sizeof(cmdarg_value_t) * n);
+
+  while (n > 0) {
+    n --;
+    values[n].src       = NULL;
+    values[n].flags     = cmdval_default;
+    values[n].value.arg = NULL;
+  }
+
+  return values;
+}
+
+//  error cmdopts_parse_internal ( startptr, endptr, commanddef )
+// Parses command arguments according to command definition.
+// Parsed string MUST be writable. Regardless of success or error, input
+// string should be considered corrupted by parsing process.
+// Even in case of error, commanddef should be passed to cmdopts_free().
+static gchar *cmdopts_parse_internal(gchar **pr, gchar **er, const cmdopts_t *command, cmdarg_value_t *values)
+{
+  enum {             // Parser state transitions:
+    in_space,        // -> in_space, in_optstart, in_argstart
+    in_optstart,     // -> in_shortoptend, in_longoptstart, in_argstart ("-")
+    in_shortoptend,  // -> in_space, in_argstart, error
+    in_longoptstart, // -> in_longopt, in_space, in_argstart ("---")
+    in_longopt,      // -> in_longopt, in_space, error
+    in_argstart,     // -> in_space, error
+  } state = in_argstart;         // current parser state
+  gchar    *p            = *pr;  // current pointer
+  gchar    *e            = *er;  // end of line pointer
+  gchar    *s;                   // start of current object pointer
+  gboolean opts_ended   = FALSE; // don't allow options any more
+  const cmdopt_t *option = NULL; // option, for which argument is currently parsed
+  size_t   argno        = 0;     // number of current positional argument
+  gchar    *error       = NULL;  // error message to return
+
+  // prepare: set default values for arguments and unset some fields
+  if (error == NULL) {
+    size_t n;
+    if (command -> opts) {
+      for (n = 0; command -> opts[n].shortopt != 0; n ++) {
+        const cmdopt_t *opt = command -> opts + n;
+        cmdarg_value_t *val = values + opt -> arg.pos;
+        // do not overwrite already specified values
+        if (val -> src == NULL || !(val -> flags & cmdval_visited)) {
+          if (opt -> arg.flags & cmdarg_switch) {
+            val -> value.swc = 0;
+          } else {
+            val -> value.roarg = opt -> arg.defval; // XXX ro
+          }
+          val -> src = &(opt -> arg);
+        }
+      }
+    }
+    if (command -> args) {
+      for (n = 0; command -> args[n].name != NULL; n ++) {
+        const cmdarg_t *arg = command -> args + n;
+        cmdarg_value_t *val = values + arg -> pos;
+        // do not overwrite already specified values
+        if (val -> src == NULL || !(val -> flags & cmdval_visited)) {
+          val -> value.roarg = arg -> defval; // XXX ro
+          val -> src         = arg;
+        }
+      }
+    }
+  }
+
+  // general environment checking
+  if (command -> check) {
+    if ((error = command -> check (command, values))) {
+      gchar *err = error;
+      error = g_strdup_printf("%s: %s", command -> name, err);
+      g_free (err);
+    }
+  }
+
+  // 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 == e) { // 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 == e) { // argument '-'
+        opts_ended = TRUE;
+        s = p = 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 == e) { // option really ended
+        size_t n;
+        for (n = 0; command -> opts[n].shortopt != 0; n ++) {
+          if (command -> opts[n].shortopt == *s) {
+            option = command -> opts + n;
+            break;
+          }
+        }
+        if (option != NULL) { // option is known
+          if (option -> arg.flags & cmdarg_switch) { // it is switch
+            cmdarg_value_t *val = values + option -> arg.pos;
+            if (val -> src != &(option -> arg)) {
+              val -> value.swc = 1;
+              val -> src       = &(option -> arg);
+            } else {
+              val -> value.swc ++;
+            }
+            val -> flags |= cmdval_visited;
+            option = NULL;
+          } else if (p == e) {
+            error = g_strdup_printf ("%s: Option -%c needs an argument.", command -> name, option -> shortopt);
+          }
+          state = in_space;
+          p ++;
+        } else { // option is unknown
+          error = g_strdup_printf ("%s: Unknown option -%c.", command -> name, *s);
+        }
+      } else { // short option not ended
+        // consider it argument (to allow -autojoin)
+        opts_ended = TRUE;
+        s = p = p - 2;
+        state = in_argstart;
+      }
+    } else if (state == in_longoptstart) { // long option initialization
+      if (*p == ' ' || p == e) { // end of options '--'
+        opts_ended = TRUE;
+        state = in_space;
+        p ++;
+      } else if (*p == '-') { // argument, starting with '---'
+        opts_ended = TRUE;
+        s = p = 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 == e) { // long option ended
+        size_t n;
+        *p = '\0';
+        for (n = 0; command -> opts[n].shortopt != 0; n ++) {
+          if (!g_strcmp0(command -> opts[n].longopt, s)) {
+            option = command -> opts + n;
+            break;
+          }
+        }
+        if (option != NULL) { // option is known
+          if (option -> arg.flags & cmdarg_switch) { // it is switch
+            cmdarg_value_t *val = values + option -> arg.pos;
+            if (val -> src != &(option -> arg)) {
+              val -> value.swc = 1;
+              val -> src       = &(option -> arg);
+            } else {
+              val -> value.swc ++;
+            }
+            val -> flags |= cmdval_visited;
+            option = NULL;
+          } else if (p == e) {
+            error = g_strdup_printf ("%s: Option --%s needs an argument.", command -> name, option -> longopt);
+          }
+          state = in_space;
+          p ++;
+        } else { // option is unknown
+          error = g_strdup_printf ("%s: Unknown option --%s.", command -> name, option -> longopt);
+        }
+      } else { // still long option
+        p ++;
+      }
+    } else if (state == in_argstart) { // option/command argument initialization
+      const char     *err;
+      const cmdarg_t *arg;
+
+      if (option) { // option argument
+        arg = &(option -> arg);
+      } else if (!command -> args || !command -> args[argno].name) { // no need to parse arguments any further
+        break;
+      } else { // normal aurgument
+        arg = command -> args + argno;
+      }
+
+      if ((err = cmdopts_parse_argument (&p, &e, arg -> flags))) { // get argument value
+        if (!option) {
+          error = g_strdup_printf ("%s: Can't parse argument %s (%lu): %s", command -> name, arg -> name, argno + 1, err);
+        } else if (option -> longopt) {
+          error = g_strdup_printf ("%s: Can't parse argument for option --%s: %s", command -> name, option -> longopt, err);
+        } else {
+          error = g_strdup_printf ("%s: Can't parse argument for option -%c: %s", command -> name, option -> shortopt, err);
+        }
+      } else {
+        cmdarg_value_t *val = values + arg -> pos;
+        val -> value.arg = s;
+        val -> src       = arg;
+        val -> flags    |= cmdval_visited;
+        if (option) { // option argument
+          option = NULL;
+        } else { // normal argument
+          if (arg -> flags & cmdarg_subcmd) { // subcommand
+            const cmdopts_t *subcmd;
+            for (subcmd = command -> cmds; subcmd -> name != NULL; subcmd ++)
+              if (!strcmp (s, subcmd -> name))
+                break;
+            if (subcmd -> name != NULL) { // found subcommand
+              gchar *err;
+              val -> value.cmd = subcmd;
+              if ((err = cmdopts_parse_internal (&p, &e, subcmd, values))) {
+                error = g_strdup_printf("%s %s", command -> name, err);
+                g_free (err);
+              }
+            } else if (arg -> flags & cmdarg_required) { // unknown subcommand
+              error = g_strdup_printf("%s: Unable to find subcommand \"%s\".", command -> name, s);
+            } else { // XXX warning message?
+              val -> value.cmd = NULL;
+            }
+          }
+          argno ++;
+        }
+      }
+      state = in_space;
+    }
+  }
+
+  *pr = p;
+  *er = e;
+
+  return error;
+}
+
+// value types:      type               check                 required
+// - switch          no effect          toboolean()           no effect?
+// - option          check              call checker          fatal check error
+// - argument        check              call checker          fatal check error
+// - subcommand      no effect          check for required    fail if missing
+static gchar *cmdopts_check_values (const cmdopts_t *command, cmdarg_value_t *values)
+{
+  size_t n = command -> valno;
+
+  if (n == 0)
+    return NULL;
+
+  // XXX reverse order?
+  while (n > 0) {
+    cmdarg_value_t *val = values + n - 1;
+    n --;
+    if ((val -> flags & cmdval_visited) || ((val -> src != NULL) && (val -> src -> flags & cmdarg_check))) {
+      if (val -> src -> flags & cmdarg_subcmd) { // subcommand - fail if required and missing
+        if ((val -> src -> flags & cmdarg_required) && (val -> value.cmd == NULL))
+          return g_strdup_printf ("%s: Not specified %s.", command -> name, val -> src -> name);
+      } else if (val -> src -> flags & cmdarg_switch) { // switch - toboolean if check
+        if (val -> src -> flags & cmdarg_check)
+          val -> value.swc %= 2;
+      } else if (val -> src -> type && val -> src -> type -> check) { // typed argument
+        gchar *err;
+        if ((err = val -> src -> type -> check (val))) {
+          // FIXME option name
+          if (val -> src -> flags & cmdarg_required) {
+            gchar *error = g_strdup_printf ("Error in argument value \"%s\": %s", val -> src -> name, err);
+            g_free (err);
+            return error;
+          } else {
+            scr_log_print (LPRINT_NORMAL, "Warning: %s: Error in argument value \"%s\": %s", command -> name, val -> src -> name, err);
+            g_free (err);
+          }
+        }
+      }
+    }
+  }
+
   return NULL;
 }
-#endif
-
-//  cmd_add()
-// Adds a command to the commands list and to the CMD completion list
-gpointer cmd_add(const char *name, const char *help, guint flags_row1,
-                 guint flags_row2, void (*f)(char*), gpointer userdata)
+
+//  cmdopts_free ( commanddef )
+// Free various parser data, used in parsing process
+static void cmdopts_free_values (const cmdopts_t *command, cmdarg_value_t *values)
 {
-  cmd *n_cmd = g_slice_new0(cmd);
-  strncpy(n_cmd->name, name, 32-1);
-  n_cmd->help = help;
-  n_cmd->completion_flags[0] = flags_row1;
-  n_cmd->completion_flags[1] = flags_row2;
-  n_cmd->func = f;
-  n_cmd->userdata = userdata;
-  Commands = g_slist_prepend(Commands, n_cmd);
-  // Add to completion CMD category
-  compl_add_category_word(COMPL_CMD, name);
-  return n_cmd;
+  size_t n = command -> valno;
+
+  if (n == 0)
+    return;
+
+  while (n > 0) {
+    cmdarg_value_t *val = values + n - 1;
+    n --;
+    if ((val -> flags & cmdval_freeme) &&
+        val -> src && val -> src -> type &&
+        val -> src -> type -> free) {
+      val -> src -> type -> free (val);
+    }
+  }
+
+  g_slice_free1 (sizeof(cmdarg_value_t) * n, values);
 }
 
-//  cmd_set_safe(name, safe)
-// Sets if command can be used in startup configuration file.
-gboolean cmd_set_safe(const gchar *name, gboolean safe)
+cmd_result_t cmd_execute (gchar *commandline, cmdexe_flags_t flags)
 {
-  GSList *sel;
-  if (!name)
-    return FALSE;
-  for (sel = safe_commands; sel; sel = sel->next)
-    if (!strcmp((const char *)sel->data, name)) {
-      if (safe)
-        return FALSE;
-      else {
-        g_free(sel->data);
-        safe_commands = g_slist_delete_link(safe_commands, sel);
-      }
+  gchar *s = commandline;
+  gchar *p, *e;
+  gchar *freeme = NULL;
+  const char *err;
+  gchar      *error;
+  cmdopts_t  *command = NULL;
+  const char *alias   = NULL;
+  cmdarg_value_t *values;
+  size_t n;
+
+  // skip command indicator and spaces at the beginning
+  while (*s == COMMAND_CHAR || *s == ' ')
+    s ++;
+  p = s;
+  e = s + strlen (s);
+
+  // separate first word - command name
+  if ((err = cmdopts_parse_argument (&p, &e, cmdarg_default))) {
+    scr_log_print (LPRINT_NORMAL, "error: Can't comprehend command name: %s.", err);
+    return cmd_result_syntax_error;
+  }
+
+  // check for quit command
+  if (!strcmp (s, "quit")) {
+    return cmd_result_quit;
+  }
+
+  // check if we're in verbatim mode
+  if ((flags & cmdexe_check_verbatim) && scr_get_multimode () == 2) {
+    if (strcmp (s, "msay")) { // it is not msay
+      return cmd_result_verbatim;
     }
-  if (safe)
-    safe_commands = g_slist_append(safe_commands, g_strdup(name));
-  else
-    return FALSE;
-  return TRUE;
+  }
+
+  // check and expand alias
+  if ((alias = settings_get (SETTINGS_TYPE_ALIAS, s))) {
+    freeme = s = g_strdup_printf ("%s %s", alias, p);
+    p = s;
+    e = s + strlen (s);
+
+    if ((err = cmdopts_parse_argument (&p, &e, cmdarg_default))) {
+      scr_log_print (LPRINT_NORMAL, "error: Can't comprehend command name: %s.", err);
+      g_free (freeme);
+      return cmd_result_syntax_error;
+    }
+
+    // check for quit command again
+    if (!strcmp (s, "quit")) {
+      g_free (freeme);
+      return cmd_result_quit;
+    }
+  }
+
+  // find command with this name (reverse - see cmd_define())
+  for (n = cmd_count; n > 0; n --) {
+    if (!strcmp (s, cmd_list[n-1] -> name)) {
+      command = cmd_list[n-1];
+      break;
+    }
+  }
+  if (command == NULL) {
+    scr_log_print (LPRINT_NORMAL, "error: Unable to find command \"%s\".", s);
+    g_free (freeme);
+    return cmd_result_not_found;
+  }
+
+  // check safety
+  if ((flags & cmdexe_check_safe) && !(command -> flags & cmd_safe)) {
+    scr_log_print (LPRINT_NORMAL, "error: Command \"%s\" is not safe to execute here.", command -> name);
+    g_free (freeme);
+    return cmd_result_not_found;
+  }
+
+  // allocate dynamic storage for arguments
+  values = cmdopts_allocate_values (command);
+
+  // parse command line
+  if ((error = cmdopts_parse_internal (&p, &e, command, values))) {
+    scr_log_print (LPRINT_NORMAL, "%s", error);
+    g_free (error);
+    cmdopts_free_values (command, values);
+    g_free (freeme);
+    return cmd_result_syntax_error;
+  }
+
+  // do type checking on arguments
+  if ((error = cmdopts_check_values (command, values))) {
+    scr_log_print (LPRINT_NORMAL, "%s: %s", command -> name, error);
+    g_free (error);
+    cmdopts_free_values (command, values);
+    g_free (freeme);
+    return cmd_result_value_error;
+  }
+
+  // execute command handler
+  if (command -> handle) {
+    if ((error = command -> handle (command, values))) {
+      scr_log_print (LPRINT_NORMAL, "%s: %s", command -> name, error);
+      g_free (error);
+      cmdopts_free_values (command, values);
+      g_free (freeme);
+      return cmd_result_runtime_error;
+    }
+  }
+
+  // free resources
+  cmdopts_free_values (command, values);
+  g_free (freeme);
+  return cmd_result_ok;
 }
 
-//  cmd_is_safe(name)
-// Returns if command is safe or not
-gboolean cmd_is_safe(const gchar *name)
+//  process_line(line)
+// Process a command/message line. If this isn't a command, this is a message
+// and it is sent to the currently selected buddy.
+// Returns 255 if the line is the /quit command, 0 on success and some other
+// error codes.
+cmd_result_t process_line(const char *line)
 {
-  GSList *sel;
-  if (!name)
-    return FALSE;
-  for (sel = safe_commands; sel; sel = sel->next)
-    if (!strcmp((const char *)sel->data, name))
-      return TRUE;
-  return FALSE;
-}
-
-//  cmd_init()
-// Commands table initialization
-// !!!
-// After changing commands names and it arguments names here, you must change
-// ones in init_bindings()!
-//
-void cmd_init(void)
-{
-  cmd_add("add", "Add a jabber user", COMPL_JID, 0, &do_add, NULL);
-  cmd_add("alias", "Add an alias", 0, 0, &do_alias, NULL);
-  cmd_add("authorization", "Manage subscription authorizations",
-          COMPL_AUTH, COMPL_JID, &do_authorization, NULL);
-  cmd_add("bind", "Add an key binding", 0, 0, &do_bind, NULL);
-  cmd_add("buffer", "Manipulate current buddy's buffer (chat window)",
-          COMPL_BUFFER, 0, &do_buffer, NULL);
-  cmd_add("chat_disable", "Disable chat mode", 0, 0, &do_chat_disable, NULL);
-  cmd_add("clear", "Clear the dialog window", 0, 0, &do_clear, NULL);
-  cmd_add("color", "Set coloring options", COMPL_COLOR, 0, &do_color, NULL);
-  cmd_add("connect", "Connect to the server", 0, 0, &do_connect, NULL);
-  cmd_add("del", "Delete the current buddy", 0, 0, &do_del, NULL);
-  cmd_add("disconnect", "Disconnect from server", 0, 0, &do_disconnect, NULL);
-  cmd_add("echo", "Display a string in the log window", 0, 0, &do_echo, NULL);
-  cmd_add("event", "Process an event", COMPL_EVENTSID, COMPL_EVENTS, &do_event,
-          NULL);
-  cmd_add("exit",  "Exit mcabber", 0, 0, &do_exit, NULL);
-  cmd_add("group", "Change group display settings",
-          COMPL_GROUP, COMPL_GROUPNAME, &do_group, NULL);
-  cmd_add("help", "Display some help", COMPL_CMD, 0, &do_help, NULL);
-  cmd_add("iline", "Manipulate input buffer", 0, 0, &do_iline, NULL);
-  cmd_add("info", "Show basic info on current buddy", 0, 0, &do_info, NULL);
-  cmd_add("module", "Manipulations with modules", COMPL_MODULE, 0, &do_module,
-          NULL);
-  cmd_add("move", "Move the current buddy to another group", COMPL_GROUPNAME,
-          0, &do_move, NULL);
-  cmd_add("msay", "Send a multi-lines message to the selected buddy",
-          COMPL_MULTILINE, 0, &do_msay, NULL);
-  cmd_add("otr", "Manage OTR settings", COMPL_OTR, COMPL_JID, &do_otr, NULL);
-  cmd_add("otrpolicy", "Manage OTR policies", COMPL_JID, COMPL_OTRPOLICY,
-          &do_otrpolicy, NULL);
-  cmd_add("pgp", "Manage PGP settings", COMPL_PGP, COMPL_JID, &do_pgp, NULL);
-  cmd_add("quit", "Exit the software", 0, 0, NULL, NULL);
-  cmd_add("rawxml", "Send a raw XML string", 0, 0, &do_rawxml, NULL);
-  cmd_add("rename", "Rename the current buddy", 0, 0, &do_rename, NULL);
-  cmd_add("request", "Send a Jabber IQ request", COMPL_REQUEST, COMPL_JID,
-          &do_request, NULL);
-  cmd_add("room", "MUC actions command", COMPL_ROOM, 0, &do_room, NULL);
-  cmd_add("roster", "Manipulate the roster/buddylist", COMPL_ROSTER, 0,
-          &do_roster, NULL);
-  cmd_add("say", "Say something to the selected buddy", 0, 0, &do_say, NULL);
-  cmd_add("say_to", "Say something to a specific buddy", COMPL_JID, 0,
-          &do_say_to, NULL);
-  cmd_add("screen_refresh", "Redraw mcabber screen", 0, 0, &do_screen_refresh,
-          NULL);
-  cmd_add("set", "Set/query an option value", 0, 0, &do_set, NULL);
-  cmd_add("source", "Read a configuration file", 0, 0, &do_source, NULL);
-  cmd_add("status", "Show or set your status", COMPL_STATUS, 0, &do_status,
-          NULL);
-  cmd_add("status_to", "Show or set your status for one recipient",
-          COMPL_JID, COMPL_STATUS, &do_status_to, NULL);
-  cmd_add("version", "Show mcabber version", 0, 0, &do_version, NULL);
-
-  cmd_set_safe("set", TRUE);
-  cmd_set_safe("bind", TRUE);
-  cmd_set_safe("alias", TRUE);
-  cmd_set_safe("pgp", TRUE);
-  cmd_set_safe("source", TRUE);
-  cmd_set_safe("status", TRUE);
-  cmd_set_safe("color", TRUE);
-  cmd_set_safe("otrpolicy", TRUE);
-  cmd_set_safe("module", TRUE);
-
-  // Status category
-  compl_add_category_word(COMPL_STATUS, "online");
-  compl_add_category_word(COMPL_STATUS, "avail");
-#ifdef WITH_DEPRECATED_STATUS_INVISIBLE
-  compl_add_category_word(COMPL_STATUS, "invisible");
-#endif
-  compl_add_category_word(COMPL_STATUS, "free");
-  compl_add_category_word(COMPL_STATUS, "dnd");
-  compl_add_category_word(COMPL_STATUS, "notavail");
-  compl_add_category_word(COMPL_STATUS, "away");
-  compl_add_category_word(COMPL_STATUS, "offline");
-  compl_add_category_word(COMPL_STATUS, "message");
-
-  // Roster category
-  compl_add_category_word(COMPL_ROSTER, "bottom");
-  compl_add_category_word(COMPL_ROSTER, "top");
-  compl_add_category_word(COMPL_ROSTER, "up");
-  compl_add_category_word(COMPL_ROSTER, "down");
-  compl_add_category_word(COMPL_ROSTER, "group_prev");
-  compl_add_category_word(COMPL_ROSTER, "group_next");
-  compl_add_category_word(COMPL_ROSTER, "hide");
-  compl_add_category_word(COMPL_ROSTER, "show");
-  compl_add_category_word(COMPL_ROSTER, "toggle");
-  compl_add_category_word(COMPL_ROSTER, "display");
-  compl_add_category_word(COMPL_ROSTER, "hide_offline");
-  compl_add_category_word(COMPL_ROSTER, "show_offline");
-  compl_add_category_word(COMPL_ROSTER, "toggle_offline");
-  compl_add_category_word(COMPL_ROSTER, "item_lock");
-  compl_add_category_word(COMPL_ROSTER, "item_unlock");
-  compl_add_category_word(COMPL_ROSTER, "item_toggle_lock");
-  compl_add_category_word(COMPL_ROSTER, "alternate");
-  compl_add_category_word(COMPL_ROSTER, "search");
-  compl_add_category_word(COMPL_ROSTER, "unread_first");
-  compl_add_category_word(COMPL_ROSTER, "unread_next");
-  compl_add_category_word(COMPL_ROSTER, "note");
-  compl_add_category_word(COMPL_ROSTER, "resource_lock");
-  compl_add_category_word(COMPL_ROSTER, "resource_unlock");
-
-  // Buffer category
-  compl_add_category_word(COMPL_BUFFER, "clear");
-  compl_add_category_word(COMPL_BUFFER, "bottom");
-  compl_add_category_word(COMPL_BUFFER, "top");
-  compl_add_category_word(COMPL_BUFFER, "up");
-  compl_add_category_word(COMPL_BUFFER, "down");
-  compl_add_category_word(COMPL_BUFFER, "search_backward");
-  compl_add_category_word(COMPL_BUFFER, "search_forward");
-  compl_add_category_word(COMPL_BUFFER, "readmark");
-  compl_add_category_word(COMPL_BUFFER, "date");
-  compl_add_category_word(COMPL_BUFFER, "%");
-  compl_add_category_word(COMPL_BUFFER, "purge");
-  compl_add_category_word(COMPL_BUFFER, "close");
-  compl_add_category_word(COMPL_BUFFER, "close_all");
-  compl_add_category_word(COMPL_BUFFER, "scroll_lock");
-  compl_add_category_word(COMPL_BUFFER, "scroll_unlock");
-  compl_add_category_word(COMPL_BUFFER, "scroll_toggle");
-  compl_add_category_word(COMPL_BUFFER, "list");
-  compl_add_category_word(COMPL_BUFFER, "save");
-
-  // Group category
-  compl_add_category_word(COMPL_GROUP, "fold");
-  compl_add_category_word(COMPL_GROUP, "unfold");
-  compl_add_category_word(COMPL_GROUP, "toggle");
-
-  // Multi-line (msay) category
-  compl_add_category_word(COMPL_MULTILINE, "abort");
-  compl_add_category_word(COMPL_MULTILINE, "begin");
-  compl_add_category_word(COMPL_MULTILINE, "send");
-  compl_add_category_word(COMPL_MULTILINE, "send_to");
-  compl_add_category_word(COMPL_MULTILINE, "toggle");
-  compl_add_category_word(COMPL_MULTILINE, "toggle_verbatim");
-  compl_add_category_word(COMPL_MULTILINE, "verbatim");
-
-  // Room category
-  compl_add_category_word(COMPL_ROOM, "affil");
-  compl_add_category_word(COMPL_ROOM, "ban");
-  compl_add_category_word(COMPL_ROOM, "bookmark");
-  compl_add_category_word(COMPL_ROOM, "destroy");
-  compl_add_category_word(COMPL_ROOM, "invite");
-  compl_add_category_word(COMPL_ROOM, "join");
-  compl_add_category_word(COMPL_ROOM, "kick");
-  compl_add_category_word(COMPL_ROOM, "leave");
-  compl_add_category_word(COMPL_ROOM, "names");
-  compl_add_category_word(COMPL_ROOM, "nick");
-  compl_add_category_word(COMPL_ROOM, "privmsg");
-  compl_add_category_word(COMPL_ROOM, "remove");
-  compl_add_category_word(COMPL_ROOM, "role");
-  compl_add_category_word(COMPL_ROOM, "setopt");
-  compl_add_category_word(COMPL_ROOM, "topic");
-  compl_add_category_word(COMPL_ROOM, "unban");
-  compl_add_category_word(COMPL_ROOM, "unlock");
-  compl_add_category_word(COMPL_ROOM, "whois");
-
-  // Authorization category
-  compl_add_category_word(COMPL_AUTH, "allow");
-  compl_add_category_word(COMPL_AUTH, "cancel");
-  compl_add_category_word(COMPL_AUTH, "request");
-  compl_add_category_word(COMPL_AUTH, "request_unsubscribe");
-
-  // Request (query) category
-  compl_add_category_word(COMPL_REQUEST, "last");
-  compl_add_category_word(COMPL_REQUEST, "ping");
-  compl_add_category_word(COMPL_REQUEST, "time");
-  compl_add_category_word(COMPL_REQUEST, "vcard");
-  compl_add_category_word(COMPL_REQUEST, "version");
-
-  // Events category
-  compl_add_category_word(COMPL_EVENTS, "accept");
-  compl_add_category_word(COMPL_EVENTS, "ignore");
-  compl_add_category_word(COMPL_EVENTS, "reject");
-
-  // PGP category
-  compl_add_category_word(COMPL_PGP, "disable");
-  compl_add_category_word(COMPL_PGP, "enable");
-  compl_add_category_word(COMPL_PGP, "force");
-  compl_add_category_word(COMPL_PGP, "info");
-  compl_add_category_word(COMPL_PGP, "setkey");
-
-  // OTR category
-  compl_add_category_word(COMPL_OTR, "start");
-  compl_add_category_word(COMPL_OTR, "stop");
-  compl_add_category_word(COMPL_OTR, "fingerprint");
-  compl_add_category_word(COMPL_OTR, "smpq");
-  compl_add_category_word(COMPL_OTR, "smpr");
-  compl_add_category_word(COMPL_OTR, "smpa");
-  compl_add_category_word(COMPL_OTR, "info");
-  compl_add_category_word(COMPL_OTR, "key");
-
-  // OTR Policy category
-  compl_add_category_word(COMPL_OTRPOLICY, "plain");
-  compl_add_category_word(COMPL_OTRPOLICY, "manual");
-  compl_add_category_word(COMPL_OTRPOLICY, "opportunistic");
-  compl_add_category_word(COMPL_OTRPOLICY, "always");
-
-  // Color category
-  compl_add_category_word(COMPL_COLOR, "roster");
-  compl_add_category_word(COMPL_COLOR, "muc");
-  compl_add_category_word(COMPL_COLOR, "mucnick");
-
-#ifdef MODULES_ENABLE
-  // Module category
-  compl_add_category_word(COMPL_MODULE, "info");
-  compl_add_category_word(COMPL_MODULE, "list");
-  compl_add_category_word(COMPL_MODULE, "load");
-  compl_add_category_word(COMPL_MODULE, "unload");
-#endif
-}
-
-//  expandalias(line)
-// If there is one, expand the alias in line and returns a new allocated line
-// If no alias is found, returns line
-// Note: if the returned pointer is different from line, the caller should
-//       g_free() the pointer after use
-char *expandalias(const char *line)
-{
-  const char *p1, *p2;
-  char *word;
-  const gchar *value;
-  char *newline = (char*)line;
-
-  // Ignore leading COMMAND_CHAR
-  for (p1 = line ; *p1 == COMMAND_CHAR ; p1++)
-    ;
-  // Locate the end of the word
-  for (p2 = p1 ; *p2 && (*p2 != ' ') ; p2++)
-    ;
-  // Extract the word and look for an alias in the list
-  word = g_strndup(p1, p2-p1);
-  value = settings_get(SETTINGS_TYPE_ALIAS, (const char*)word);
-  g_free(word);
-
-  if (value)
-    newline = g_strdup_printf("%c%s%s", COMMAND_CHAR, value, p2);
-
-  return newline;
-}
-
-//  cmd_get
-// Finds command in the command list structure.
-// Returns a pointer to the cmd entry, or NULL if command not found.
-cmd *cmd_get(const char *command)
-{
-  const char *p1, *p2;
-  char *com;
-  GSList *sl_com;
-
-  // Ignore leading COMMAND_CHAR
-  for (p1 = command ; *p1 == COMMAND_CHAR ; p1++)
-    ;
-  // Locate the end of the command
-  for (p2 = p1 ; *p2 && (*p2 != ' ') ; p2++)
-    ;
-  // Copy the clean command
-  com = g_strndup(p1, p2-p1);
-
-  // Look for command in the list
-  for (sl_com=Commands; sl_com; sl_com = g_slist_next(sl_com)) {
-    if (!strcasecmp(com, ((cmd*)sl_com->data)->name))
-      break;
-  }
-  g_free(com);
-
-  if (sl_com)       // Command has been found.
-    return (cmd*)sl_com->data;
-  return NULL;
-}
-
-//  process_command(line, iscmd)
-// Process a command line.
-// If iscmd is TRUE, process the command even if verbatim mmode is set;
-// it is intended to be used for key bindings.
-// Return 255 if this is the /quit command, and 0 for the other commands.
-int process_command(const char *line, guint iscmd)
-{
-  char *p;
-  char *xpline;
-  cmd *curcmd;
-
-  if (!line)
-    return 0;
-
-  // We do alias expansion here
-  if (iscmd || scr_get_multimode() != 2)
-    xpline = expandalias(line);
-  else
-    xpline = (char*)line; // No expansion in verbatim multi-line mode
-
-  // We want to use a copy
-  if (xpline == line)
-    xpline = g_strdup(line);
-
-  // Remove trailing spaces:
-  for (p=xpline ; *p ; p++)
-    ;
-  for (p-- ; p>xpline && (*p == ' ') ; p--)
-    *p = 0;
-
-  // Command "quit"?
-  if ((iscmd || scr_get_multimode() != 2)
-      && (!strncasecmp(xpline, mkcmdstr("quit"), strlen(mkcmdstr("quit"))))) {
-    if (!xpline[5] || xpline[5] == ' ') {
-      g_free(xpline);
-      return 255;
-    }
-  } else if (iscmd && !strncasecmp(xpline, "quit", 4) &&
-             (!xpline[4] || xpline[4] == ' ')) {
-    // If iscmd is true we can have the command without the command prefix
-    // character (usually '/').
-    g_free(xpline);
-    return 255;
-  }
-
-  // If verbatim multi-line mode, we check if another /msay command is typed
-  if (!iscmd && scr_get_multimode() == 2
-      && (strncasecmp(xpline, mkcmdstr("msay "), strlen(mkcmdstr("msay "))))) {
-    // It isn't an /msay command
-    scr_append_multiline(xpline);
-    g_free(xpline);
-    return 0;
-  }
-
-  // Commands handling
-  curcmd = cmd_get(xpline);
-
-  if (!curcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized command.  "
-                 "Please see the manual for a list of known commands.");
-    g_free(xpline);
-    return 0;
-  }
-  if (!curcmd->func) {
-    scr_LogPrint(LPRINT_NORMAL,
-                 "This functionality is not yet implemented, sorry.");
-    g_free(xpline);
-    return 0;
-  }
-  // Lets go to the command parameters
-  for (p = xpline+1; *p && (*p != ' ') ; p++)
-    ;
-  // Skip spaces
-  while (*p && (*p == ' '))
-    p++;
-  // Call command-specific function
-  retval_for_cmds = 0;
-#ifdef MODULES_ENABLE
-  if (curcmd->userdata)
-    (*(void (*)(char *p, gpointer u))curcmd->func)(p, curcmd->userdata);
-  else
-    (*curcmd->func)(p);
-#else
-  (*curcmd->func)(p);
-#endif
-  g_free(xpline);
-  return retval_for_cmds;
-}
-
-//  process_line(line)
-// Process a command/message line.
-// If this isn't a command, this is a message and it is sent to the
-// currently selected buddy.
-// Return 255 if the line is the /quit command, or 0.
-int process_line(const char *line)
-{
+  gchar *utf8;
+  int retval;
+
   if (!*line) { // User only pressed enter
     if (scr_get_multimode()) {
       scr_append_multiline("");
@@ -556,141 +724,706 @@
     }
     if (current_buddy) {
       if (buddy_gettype(BUDDATA(current_buddy)) & ROSTER_TYPE_GROUP)
-        do_group("toggle");
+        group_cmd (BUDDATA(current_buddy), scmd_group_toggle);
       else {
         // Enter chat mode
         scr_set_chatmode(TRUE);
         scr_show_buddy_window();
       }
     }
-    return 0;
+    return cmd_result_input;
   }
 
-  if (*line != COMMAND_CHAR) {
-    // This isn't a command
+  utf8 = to_utf8(line);
+  if (*line != COMMAND_CHAR) { // input line
     if (scr_get_multimode())
-      scr_append_multiline(line);
+      scr_append_multiline(utf8);
     else
-      say_cmd((char*)line, 0);
-    return 0;
-  }
-
-  /* It is _probably_ a command -- except for verbatim multi-line mode */
-  return process_command(line, FALSE);
-}
-
-// Helper routine for buffer item_{lock,unlock,toggle_lock}
-// "lock" values: 1=lock 0=unlock -1=invert
-static void roster_buddylock(char *bjid, int lock)
-{
-  gpointer bud = NULL;
-  bool may_need_refresh = FALSE;
-
-  // Allow special jid "" or "." (current buddy)
-  if (bjid && (!*bjid || !strcmp(bjid, ".")))
-    bjid = NULL;
-
-  if (bjid) {
-    // The JID has been specified.  Quick check...
-    if (check_jid_syntax(bjid)) {
-      scr_LogPrint(LPRINT_NORMAL|LPRINT_NOTUTF8,
-                   "<%s> is not a valid Jabber ID.", bjid);
-    } else {
-      // Find the buddy
-      GSList *roster_elt;
-      roster_elt = roster_find(bjid, jidsearch,
-                               ROSTER_TYPE_USER|ROSTER_TYPE_ROOM);
-      if (roster_elt)
-        bud = roster_elt->data;
-      else
-        scr_LogPrint(LPRINT_NORMAL, "This jid isn't in the roster.");
-      may_need_refresh = TRUE;
-    }
-  } else {
-    // Use the current buddy
-    if (current_buddy)
-      bud = BUDDATA(current_buddy);
-  }
-
-  // Update the ROSTER_FLAG_USRLOCK flag
-  if (bud) {
-    if (lock == -1)
-      lock = !(buddy_getflags(bud) & ROSTER_FLAG_USRLOCK);
-    buddy_setflags(bud, ROSTER_FLAG_USRLOCK, lock);
-    if (may_need_refresh) {
-      buddylist_build();
-      update_roster = TRUE;
+      say_cmd(utf8, msgtype_not_set);
+    retval = 0;
+  } else { // command or verbatim multiline message
+    retval = cmd_execute (utf8, cmdexe_check_verbatim);
+    if (retval == cmd_result_verbatim) {
+      g_free(utf8); // buffer has been corrupted by parser
+      utf8 = to_utf8(line);
+      scr_append_multiline(utf8);
+      retval = cmd_result_input;
     }
   }
+  g_free(utf8);
+
+  return retval;
 }
 
-static void roster_resourcelock(char *jidres, gboolean lock) {
-  gpointer bud = NULL;
-  char *resource = NULL;
-
-  if (!jidres) {
-    if (lock) return;
-    jidres = ".";
-  }
-
-  if (jidres[0] == '.' &&
-      (jidres[1] == '\0' || jidres[1] == JID_RESOURCE_SEPARATOR)) {
-    //Special jid: . or ./resource
-    switch (jidres[1]) {
-      case JID_RESOURCE_SEPARATOR:
-        resource = jidres+2;
-      case '\0':
-        if (current_buddy)
-          bud = BUDDATA(current_buddy);
-    }
-  } else {
-    char *tmp;
-    if (!check_jid_syntax(jidres) &&
-        (tmp = strchr(jidres, JID_RESOURCE_SEPARATOR))) {
-      //Any other valid full jid
-      *tmp = '\0'; // for roster search by bare jid;
-      resource = tmp+1;
-      GSList *roster_elt;
-      roster_elt = roster_find(jidres, jidsearch,
-          ROSTER_TYPE_USER|ROSTER_TYPE_AGENT);
-      if (roster_elt)
-        bud = roster_elt->data;
-      *tmp = JID_RESOURCE_SEPARATOR;
-    }
-    if (!bud) {
-      //Resource for current buddy
-      if (current_buddy)
-        bud = BUDDATA(current_buddy);
-      resource = jidres;
+//
+//  Standard types
+//
+// This section contains standard argument type definitions, used in built-in
+// mcabber commands.
+//
+
+// TODO: move to separate file? (and variations with 'required')
+// + cmdarg_type_buddy        - in roster, with specified types, resource/activeresource/group -> bud + (resource)
+// + cmdarg_type_resource     - in roster, with specified types, have resource -> bud + resource
+// + cmdarg_type_bjid         - any bjid -> bjid
+// + cmdarg_type_fjid         - any fjid -> fjid
+// + cmdarg_type_charset      - string -> string
+// + cmdarg_type_uint         - string -> unsigned long
+// + cmdarg_type_sint         - string -> signed long (unused)
+// + cmdarg_type_nonspace     - strip, space only -> null
+// * cmdarg_type_bjidmask
+// + cmdarg_type_color
+// + cmdarg_type_string2enum
+// * cmdarg_type_nick        - provide completions first from current room, then from all other, nonspace, do not restrict
+// + cmdarg_type_filename    - expand, convert encoding
+// + cmdarg_type_date        - YYYYMMDDTHHMMSS -> time_t
+// * cmdarg_type_assignment  - string -> key + value
+
+//
+//  command environment checkers
+//
+
+gchar *cmd_check_online (const cmdopts_t *command, cmdarg_value_t *values)
+{
+  if (!xmpp_is_online())
+    return g_strdup ("You are not connected!");
+  return NULL;
+}
+
+gchar *cmd_check_current_buddy (const cmdopts_t *command, cmdarg_value_t *values)
+{
+  if (!current_buddy)
+    return g_strdup ("No selected buddy!");
+  return NULL;
+}
+
+//
+//  generic value destructors
+//
+
+void cmdarg_free_gfree (cmdarg_value_t *arg)
+{
+  g_free (arg -> value.arg);
+}
+
+//
+//  string -> stripspace string
+//
+
+// Strips leading and trailing spaces, checks if anything left.
+// defvalue: no trailing spaces.
+gchar *cmdarg_check_nonspace (cmdarg_value_t *arg)
+{
+  gchar *val = arg -> value.arg;
+
+  if (val) {
+    while (isspace (*val))
+      val ++;
+    if (*val) { // valid arg
+      arg -> value.arg = val;
+      while (*val)
+        val ++;
+      while (isspace (*val))
+        val --;
+      val ++;
+      if (*val)
+        *val = '\0';
+      return NULL;
     }
   }
-  
-  if (bud && buddy_gettype(bud) & (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT)) {
-    if (lock) {
-      GSList *resources, *p_res;
-      gboolean found = FALSE;
-      resources = buddy_getresources(bud);
-      for (p_res = resources ; p_res ; p_res = g_slist_next(p_res)) {
-        if (!g_strcmp0((char*)p_res->data, resource))
-          found = TRUE;
-        g_free(p_res->data);
+
+  // error
+  arg -> value.arg = NULL;
+  return g_strdup ("Non-space value required.");
+}
+
+const cmdarg_type_t cmdarg_type_nonspace = {
+  cmdarg_check_nonspace,
+  NULL,
+  NULL,
+};
+
+//
+//  roster name/jid -> bud + resource
+//
+
+// chkdata: cmdarg_roster_t
+// returns: value.rjid
+// defvalue: no "user@[/res]", no "jid/resource"
+// XXX: activeres/group stuff can easily go to separate checkers
+//
+// Flags:
+//  - name
+//  - activeres
+//  - getgroup
+//
+// name:
+//  1. .    -> bud
+//  1. name -> bud
+//  2. bud  -> bud/activeres
+//  2. bud  -> groupbud
+// jid:
+//  1. user@[/res]    -> jid[/res]
+//  2. jid[/res]      -> checkjid[/res]
+//  3. checkjid[/res] -> bud[/res]
+//  3. .[/res]        -> bud[/res]
+//  4. bud            -> groupbud
+//  4. bud            -> bud/activeres
+//  4. bud/res        -> bud/checkres
+gchar *cmdarg_check_buddy (cmdarg_value_t *arg)
+{
+  gchar    *error    = NULL;
+  gpointer bud       = NULL;
+  char     *resource = NULL;
+
+  if (!(error = cmdarg_check_nonspace(arg))) {
+    char  *fjid = arg -> value.arg;
+    const cmdarg_roster_t flags = (guint) arg -> src -> chkdata;
+
+    // it is name
+    if (flags & cmdarg_roster_name) {
+      if (!(fjid[0] == '.' && fjid[1] == '\0')) {
+        GSList *found = roster_find (fjid, namesearch, flags & cmdarg_roster_mask);
+        if (found) {
+          bud = found->data;
+        } else {
+          error = g_strdup_printf ("Name \"%s\" is not in the roster.", fjid);
+        }
       }
-      g_slist_free(resources);
-      if (!found) {
-        scr_LogPrint(LPRINT_NORMAL, "No such resource <%s>...", jidres);
-        return;
+    } else if (fjid[0] == '.' && (fjid[1] == JID_RESOURCE_SEPARATOR || fjid[1] == '\0')) {
+      // current buddy/jid
+      if (fjid[1] == JID_RESOURCE_SEPARATOR)
+        resource = fjid+2;
+    } else { // plain jid
+      const char *server = settings_opt_get ("default_server");
+      gchar      *freeme = NULL;
+      // exand @-expression
+      if (server != NULL) {
+        char *domain = strchr (fjid, JID_DOMAIN_SEPARATOR);
+        if (domain && (domain[1] == JID_RESOURCE_SEPARATOR || domain[1] == '\0')) {
+          if (domain[1] == JID_RESOURCE_SEPARATOR)
+            // use resource from original value
+            resource = domain + 2;
+          *domain  = '\0';
+          freeme = fjid = g_strdup_printf ("%s" JID_DOMAIN_SEPARATORSTR "%s%s", fjid, server, domain + 1);
+        }
+      }
+      if (!check_jid_syntax (fjid)) {
+        // jid is valid - search for buddy
+        GSList *found;
+        char   *res = strchr (fjid, JID_RESOURCE_SEPARATOR);
+        if (res != NULL) {
+          *res = '\0';
+          if (resource == NULL) {
+            resource = res + 1;
+          }
+        }
+        found = roster_find (fjid, jidsearch, flags & cmdarg_roster_mask);
+        if (found) {
+          bud = found->data;
+        } else {
+          error = g_strdup_printf ("Jid <%s> is not in the roster.", fjid);
+        }
+      } else {
+        error = g_strdup_printf ("<%s> is not a valid jid.", fjid);
+      }
+      g_free (freeme);
+    }
+  }
+  // fjid should not be used anymore!
+  if (error == NULL) {
+    if (bud == NULL) {
+      // it is current buddy
+      if (!current_buddy)
+        error = g_strdup ("No buddy selected.");
+      else if (buddy_gettype (BUDDATA(current_buddy)) & (flags & cmdarg_roster_mask))
+        bud = BUDDATA(current_buddy);
+      else // TODO: improve message
+        error = g_strdup("Currently selected buddy is of wrong type.");
+    }
+    if (error == NULL) {
+      if (resource == NULL) {
+        // no resource given - set active resource
+        if (flags & cmdarg_roster_getgroup) {
+          bud = buddy_getgroup (bud);
+        } else if (error == NULL && (flags & cmdarg_roster_activeres)) {
+          resource = buddy_getactiveresource (bud);
+        }
+      } else {
+        // check resource presence
+        GSList *resources, *p_res;
+        gboolean found = FALSE;
+        resources = buddy_getresources (bud);
+        for (p_res = resources; p_res; p_res = g_slist_next(p_res)) {
+          if (!g_strcmp0 ((char*)p_res->data, resource))
+            found = TRUE;
+          g_free (p_res->data);
+        }
+        g_slist_free (resources);
+        if (!found) {
+          error = g_strdup_printf ("No such resource <%s" JID_RESOURCE_SEPARATORSTR "%s>...", buddy_getjid(bud), resource);
+        }
+      }
+    }
+  }
+
+  if (error) {
+    arg -> value.rjid.bud      = NULL;
+    arg -> value.rjid.resource = NULL;
+  } else {
+    arg -> value.rjid.bud      = bud;
+    arg -> value.rjid.resource = resource;
+  }
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_buddy = {
+  cmdarg_check_buddy,
+  NULL,
+  NULL,
+};
+
+//
+//  fjid -> fjid
+//
+
+// chkdata: 0 or types for current buddy checking.
+// returns: expanded fjid in value.arg.
+// defvalue: no "user@" or "user@/res".
+// freeing: sometimes g_free.
+// Recognizes as current "." and "./res".
+// Expands "user@" and "user@/res", using default_server.
+gchar *cmdarg_check_fjid (cmdarg_value_t *arg)
+{
+  gchar *error = NULL;
+
+  if (!(error = cmdarg_check_nonspace(arg))) {
+    const char *fjid = arg -> value.arg;
+
+    if (fjid[0] == '.' && (fjid[1] == JID_RESOURCE_SEPARATOR || fjid[1] == '\0')) {
+      const char *jid;
+      const cmdarg_roster_t types = (cmdarg_roster_t) (arg -> src -> userdata);
+      if (!current_buddy) {
+        error = g_strdup_printf ("No buddy selected.");
+      } else if ((types != 0) && !(buddy_gettype(BUDDATA(current_buddy)) & (types & cmdarg_roster_mask))) {
+        error = g_strdup ("Current buddy is of wrong type.");
+      } else if (!(jid = buddy_getjid(BUDDATA(current_buddy)))) {
+        error = g_strdup_printf ("Current buddy have no jid.");
+      } else if (fjid[1] == '\0') {
+        arg -> value.roarg = jid;
+      } else {
+        arg -> value.arg = g_strdup_printf ("%s" JID_RESOURCE_SEPARATORSTR "%s",
+                                            jid, fjid + 2);
+        arg -> flags    |= cmdval_freeme;
       }
     } else {
-      resource = NULL;
+      const char *server = settings_opt_get ("default_server");
+      if (server != NULL) {
+        const char *end;
+        if ((end = strchr (fjid, JID_DOMAIN_SEPARATOR))) {
+          end ++;
+          if (*end == '\0' || *end == JID_RESOURCE_SEPARATOR) {
+            *(end - 1) = '\0';
+            arg -> value.arg = g_strdup_printf ("%s" JID_DOMAIN_SEPARATORSTR 
+                                                "%s%s", fjid, server, end);
+            arg -> flags    |= cmdval_freeme;
+          }
+        }
+      }
+      if (check_jid_syntax(fjid))
+        error = g_strdup_printf ("Jid <%s> is invalid.", fjid);
     }
-    buddy_setactiveresource(bud, resource);
-    scr_update_chat_status(TRUE);
+  }
+
+  if (error) {
+    if (arg -> flags & cmdval_freeme)
+      g_free (arg -> value.arg);
+    arg -> value.arg = NULL;
+  }
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_fjid = {
+  cmdarg_check_fjid,
+  cmdarg_free_gfree,
+  NULL,
+};
+
+//
+//  fjid -> bjid
+//
+
+// chkdata: 0 or types for current buddy checking.
+// returns: expanded bjid in value.arg.
+// defvalue: no "user@", "user@/res" or "jid/res".
+// freeing: sometimes g_free.
+// Recognizes as current "." and "./res" (but still removes resource).
+// Expands "user@" and "user@/res", using default_server.
+gchar *cmdarg_check_bjid (cmdarg_value_t *arg)
+{
+  gchar *error = NULL;
+
+  if (!(error = cmdarg_check_fjid(arg))) {
+    gchar *res = strchr (arg -> value.arg, JID_RESOURCE_SEPARATOR);
+    if (res != NULL)
+      *res = '\0';
+  }
+
+  if (error)
+    arg -> value.arg = NULL;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_bjid = {
+  cmdarg_check_bjid,
+  cmdarg_free_gfree,
+  NULL,
+};
+
+//
+//  string -> unsigned long
+//
+
+// chkdata: 0 or maximum value.
+// returns: unsigned long in value.uint.
+gchar *cmdarg_check_uint (cmdarg_value_t *arg)
+{
+  gchar *error;
+
+  if (!(error = cmdarg_check_nonspace(arg))) {
+    char *s = arg -> value.arg;
+    char *e = s;
+    unsigned long m = (unsigned long) (arg -> src -> chkdata);
+    unsigned long n = strtoul (s, &e, 0);
+    if (*e != '\0')
+      error = g_strdup_printf ("Invalid number \"%s\".", s);
+    else if (m > 0 && n > m)
+      error = g_strdup_printf ("Value %lu is too big (max %lu).", n, m);
+    else
+      arg -> value.uint = n;
+  }
+
+  if (error)
+    arg -> value.uint = 0;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_uint = {
+  cmdarg_check_uint,
+  NULL,
+  NULL,
+};
+
+//
+//  string -> signed long
+//
+
+// chkdata: 0 or maximum/minimum +/- value.
+// returns: long in value.sint.
+gchar *cmdarg_check_sint (cmdarg_value_t *arg)
+{
+  gchar *error;
+
+  if (!(error = cmdarg_check_nonspace(arg))) {
+    char *s = arg -> value.arg;
+    char *e = s;
+    long m  = (long) (arg -> src -> chkdata);
+    long n  = strtol (s, &e, 0);
+    if (*e != '\0')
+      error = g_strdup_printf ("Invalid number \"%s\".", s);
+    else if (m > 0 && n > m)
+      error = g_strdup_printf ("Value %lu is too big (max %lu).", n, m);
+    else if (m > 0 && n < -m)
+      error = g_strdup_printf ("Value %lu is too small (min %lu).", n, -m);
+    else
+      arg -> value.sint = n;
+  }
+
+  if (error)
+    arg -> value.sint = 0;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_sint = {
+  cmdarg_check_sint,
+  NULL,
+  NULL,
+};
+
+//
+//  string -> set of valid chars
+//
+
+// chkdata: string of valid characters.
+// returns: stripped of invalid characters value in value.arg.
+// Expands "*" to full set of valid characters.
+// defvalue: no invalid characters.
+// XXX:
+//  * check duplicated characters - string2flags?
+//  * sort & canonicalize - string2enum?
+gchar *cmdarg_check_charset (cmdarg_value_t *arg)
+{
+  gchar *error;
+
+  if (!(error = cmdarg_check_nonspace(arg)) && arg -> value.arg) {
+    const char *valid = arg -> src -> chkdata;
+    if (!strcmp(arg -> value.arg, "*")) {
+      arg -> value.roarg = valid;
+    } else {
+      gchar *p = arg -> value.arg;
+      gchar *e = p + strlen (p);
+      while (p < e) {
+        if (strchr (valid, *p)) {
+          p ++;
+        } else if (arg -> flags & cmdarg_required) {
+          // this is valid use of flag in checker
+          arg -> value.arg = NULL;
+          return g_strdup_printf ("Character '%c' not in set [%s].", *p, valid);
+        } else {
+          scr_log_print (LPRINT_NORMAL, "Warning: Wrong %s character [%c]", arg -> src -> name, *p);
+          g_memmove (p, p+1, e-p-1);
+          e --;
+        }
+      }
+      if (arg -> value.arg == e) // arg is not required and we deleted all string
+        arg -> value.arg = NULL;
+    }
+  }
+
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_charset = {
+  cmdarg_check_charset,
+  NULL,
+  NULL,
+};
+
+//
+//  string -> enum
+//
+
+// chkdata: array of string2enum_t structs.
+// returns: corresponding value in value.uint.
+// errvalue: terminator (NULL) entry value.
+// XXX: print list of values on required error?
+gchar *cmdarg_check_string2enum (cmdarg_value_t *arg)
+{
+  gchar *error = cmdarg_check_nonspace(arg);
+  const string2enum_t *list;
+
+  for (list = arg -> src -> chkdata; list -> name != NULL; list ++) {
+    if (error == NULL) {
+      if (!strcmp(list -> name, arg -> value.arg)) { // found
+        arg -> value.uint = list -> value;
+        return NULL;
+      }
+    }
+  }
+
+  // error or not found
+  if (!error)
+    error = g_strdup_printf ("Value \"%s\" is invalid.", arg -> value.arg);
+  arg -> value.uint = list -> value;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_string2enum = {
+  cmdarg_check_string2enum,
+  NULL,
+  NULL,
+};
+
+//
+//  string -> color name
+//
+
+static const string2enum_t s2e_color[] = {
+  { "default", -1 },
+  { "black",   1  },
+  { "red",     1  },
+  { "green",   1  },
+  { "yellow",  1  },
+  { "blue",    1  },
+  { "magenta", 1  },
+  { "white",   1  },
+  { NULL,      0  },
+};
+
+// returns: color name in value.arg.
+// errvalue: NULL.
+// Recognizes "-" for reset, prefix "bright", standard names and numerical values.
+// XXX: can generate ccolor, but that needs access to ncurses internals.
+gchar *cmdarg_check_color (cmdarg_value_t *arg)
+{
+  gchar *error;
+
+  if (!(error = cmdarg_check_nonspace(arg))) {
+    char *color = arg -> value.arg;
+    // reset color
+    if (!strcmp(color, "-")) {
+      return NULL;
+    }
+    // allow "bright" prefix
+    if (!strncmp(color, "bright", 6))
+      color += 6;
+    { // check names
+      const string2enum_t *list;
+      for (list = s2e_color; list -> name != NULL; list ++)
+        if (!strcmp (list -> name, color))
+          return NULL;
+    }
+    { // not found, check for numerical value
+      char *e = color;
+      long n  = strtol(color, &e, 0);
+      if (*e != '\0' || n < 0 || n > 256)
+        error = g_strdup_printf ("Invalid color \"%s\".", arg -> value.arg);
+    }
+  }
+
+  if (error)
+    arg -> value.arg = NULL;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_color = {
+  cmdarg_check_color,
+  NULL,
+  NULL,
+};
+
+//
+//  filename -> expanded local + utf8 filename
+//
+
+// Recognizes "~/" as $HOME.
+// returns: utf8 + local fs names in value.fname.
+// freeing: always, double-gfree.
+// errvalue: NULL + NULL.
+// XXX:
+//  * Should we convert filename at all?
+//    - it needs extra "big" type in values
+//    - it is not convenient to pass to function
+//    - utf8 version needs to be always freed
+//    - saves a bit of generic code, though.
+//  * We can use g_filename_display_basename() to get name back,
+//    but then we need absolute filename, and re-conversion... :(
+//    - we can provide display_basename
+//  * Can avoid g_freeing if filename does not need expansion
+gchar *cmdarg_check_filename (cmdarg_value_t *value)
+{
+  gchar  *error;
+  GError *err   = NULL;
+
+  if ((error = cmdarg_check_nonspace(value)))
+    return error;
+
+/* make path absolute
+  gchar *name     = value -> value.arg;
+  gchar *absolute = NULL;
+  if (name[0] == '~' && name[1] == '/') {
+    const char *home = getenv ("HOME");
+    if (home)
+      name = absolute = g_strdup_printf ("%s%s", home, name + 1);
+    else
+      return g_strdup ("Unable to expand filename - $HOME not set!");
+  } else if (!g_path_is_absolute (name)) {
+    gchar *cwd = g_get_current_dir ();
+    name = absolute = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s",
+                                                 cwd, name);
+    g_free (cwd);
+  }
+
+  value -> value.fname.display = g_filename_display_basename (name);
+  g_free (absolute);
+*/
+
+  value -> value.fname.utf8  = expand_filename (value -> value.arg);
+  value -> value.fname.local = g_filename_from_utf8 (value -> value.fname.utf8,
+                                                          -1, NULL, NULL, &err);
+  if (err) {
+    g_free (value -> value.fname.utf8);
+    value -> value.fname.utf8 = NULL;
+    error = g_strdup_printf ("Filename charset conversion error: %s",
+                                                                err -> message);
+    g_error_free (err);
+  } else {
+    value -> flags |= cmdval_freeme;
+  }
+
+  return error;
+}
+
+void cmdarg_free_fname (cmdarg_value_t *value)
+{
+  g_free (value -> value.fname.utf8);
+  g_free (value -> value.fname.local);
+}
+
+const cmdarg_type_t cmdarg_type_filename = {
+  cmdarg_check_color,
+  cmdarg_free_fname,
+  NULL,
+};
+
+//
+//  ISO-8601 date -> epoch
+//
+
+// Converts "YYYYMMDD[(.|T)HH[:]MM[:]SS[.SSS][(+|-)HH:MM]" to epoch.
+// returns: epoch in value.time.
+// errvalue: 0.
+gchar *cmdarg_check_date (cmdarg_value_t *value)
+{
+  gchar  *error;
+  time_t epoch = 0;
+
+  if (!(error = cmdarg_check_nonspace (value))) {
+    epoch = from_iso8601 (value -> value.arg, 0);
+    if (!epoch)
+      error = g_strdup_printf ("Invalid date \"%s\".", value -> value.arg);
+  }
+
+  value -> value.time = epoch;
+  return error;
+}
+
+const cmdarg_type_t cmdarg_type_date = {
+  cmdarg_check_date,
+  NULL,
+  NULL,
+};
+
+//
+//  Command definitions
+//
+// This section contains definitions for built-in mcabber commands.
+// If you add a definition here, that should be automatically loaded,
+// you also have to add it to cmd_init().
+//
+
+//
+//  /roster
+//
+
+// Helper routine for /roster item_{lock,unlock,toggle_lock}
+// "lock" values: 1=lock 0=unlock -1=invert
+static void roster_buddylock(gpointer bud, int lock)
+{
+  // Update the ROSTER_FLAG_USRLOCK flag
+  if (lock == -1)
+    lock = !(buddy_getflags(bud) & ROSTER_FLAG_USRLOCK);
+  buddy_setflags(bud, ROSTER_FLAG_USRLOCK, lock);
+  if ((!current_buddy) || bud == BUDDATA(current_buddy)) {
+    buddylist_build();
+    update_roster = TRUE;
   }
 }
+
 //  display_and_free_note(note, winId)
 // Display the note information in the winId buffer, and free note
 // (winId is a bare jid or NULL for the status window, in which case we
-// display the note jid too)
+// display the note jid too).
 static void display_and_free_note(struct annotation *note, const char *winId)
 {
   gchar tbuf[128];
@@ -755,41 +1488,15 @@
   g_slist_free(notes);
 }
 
-static void roster_note(char *arg)
+static void roster_note(gpointer bud, gboolean reset, gchar *note)
 {
-  const char *bjid;
-  guint type;
-
-  if (!current_buddy)
-    return;
-
-  bjid = buddy_getjid(BUDDATA(current_buddy));
-  type = buddy_gettype(BUDDATA(current_buddy));
-
-  if (!bjid && type == ROSTER_TYPE_SPECIAL && !arg) {
-    // We're in the status window (the only special buffer currently)
-    // Let's display all server notes
-    display_all_annotations();
-    return;
-  }
-
-  if (!bjid || (type != ROSTER_TYPE_USER &&
-               type != ROSTER_TYPE_ROOM &&
-               type != ROSTER_TYPE_AGENT)) {
-    scr_LogPrint(LPRINT_NORMAL, "This item can't have a note.");
-    return;
-  }
-
-  if (arg && *arg) {  // Set a note
-    gchar *msg, *notetxt;
-    msg = to_utf8(arg);
-    if (!strcmp(msg, "-"))
-      notetxt = NULL; // delete note
-    else
-      notetxt = msg;
-    xmpp_set_storage_rosternotes(bjid, notetxt);
-    g_free(msg);
-  } else {      // Display a note
+  const char *bjid = buddy_getjid(bud);
+
+  if (note) // set note
+    xmpp_set_storage_rosternotes(bjid, note);
+  else if (reset) // delete note
+    xmpp_set_storage_rosternotes(bjid, NULL);
+  else { // display a note
     struct annotation *note = xmpp_get_storage_rosternotes(bjid, FALSE);
     if (note) {
       display_and_free_note(note, bjid);
@@ -800,484 +1507,661 @@
   }
 }
 
-//  roster_updown(updown, nitems)
-// updown: -1=up, +1=down
-inline static void roster_updown(int updown, char *nitems)
+static gchar *do_roster (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  scmd_roster_bottom, scmd_roster_top, scmd_roster_up, scmd_roster_down,
+  scmd_roster_group_prev, scmd_roster_group_next,
+  scmd_roster_alternate,
+  scmd_roster_unread_first, scmd_roster_unread_next,
+  scmd_roster_search,
+  scmd_roster_display,
+  scmd_roster_hide_offline, scmd_roster_show_offline, scmd_roster_toggle_offline,
+  scmd_roster_item_lock, scmd_roster_item_unlock, scmd_roster_item_toggle_lock,
+  scmd_roster_note, scmd_roster_notes,
+  scmd_roster_resource_lock, scmd_roster_resource_unlock,
+  scmd_roster_hide, scmd_roster_show, scmd_roster_toggle,
+} scmd_roster_t;
+
+typedef enum {
+  pos_roster_scmd         = 0,
+  pos_roster_up_n         = 1,
+  pos_roster_down_n       = 1,
+  pos_roster_search_name  = 1,
+  pos_roster_display_mask = 1,
+  pos_roster_itemlock_jid = 1,
+  pos_roster_note_jid     = 1,
+  pos_roster_note_rst     = 2,
+  pos_roster_note_text    = 3,
+  pos_roster_reslock_jid  = 1,
+} pos_roster_t;
+
+#define SCMD_ROSTER(NAME, ARGS...) \
+    { #NAME, cmd_default, NULL, NULL, NULL, ARGS, NULL, (gpointer)scmd_roster_##NAME }
+static cmdopts_t def_roster = {
+  "roster",
+  cmd_default,
+  NULL,
+  do_roster,
+  NULL,
+  (cmdarg_t[2]){{"subcommand", pos_roster_scmd, cmdarg_subcmd | cmdarg_chreq, NULL, NULL},{NULL}},
+  (cmdopts_t[25]){
+    SCMD_ROSTER(bottom, NULL),
+    SCMD_ROSTER(top,    NULL),
+    SCMD_ROSTER(up,   (cmdarg_t[2]){{"n", pos_roster_up_n, cmdarg_chreq, "1", &cmdarg_type_uint, (gpointer)0},{NULL}}),
+    SCMD_ROSTER(down, (cmdarg_t[2]){{"n", pos_roster_down_n, cmdarg_chreq, "1", &cmdarg_type_uint (gpointer)0},{NULL}}),
+    SCMD_ROSTER(group_prev, NULL),
+    SCMD_ROSTER(group_next, NULL),
+    SCMD_ROSTER(alternate, NULL),
+    SCMD_ROSTER(unread_first, NULL),
+    SCMD_ROSTER(unread_next,  NULL),
+    SCMD_ROSTER(search, (cmdarg_t[2]){{"name", pos_roster_search_name, cmdarg_eol|cmdarg_required, NULL, &cmdarg_type_nonspace},{NULL}}),
+    SCMD_ROSTER(display, (cmdarg_t[2]){{"statusmask", pos_roster_display_mask, cmdarg_check, NULL, &cmdarg_type_charset, (gpointer)"ofdna_?"},{NULL}}),
+    SCMD_ROSTER(hide_offline,   NULL),
+    SCMD_ROSTER(show_offline,   NULL),
+    SCMD_ROSTER(toggle_offline, NULL),
+    SCMD_ROSTER(item_lock,        (cmdarg_t[2]){{"jid", pos_roster_itemlock_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_entity},{NULL}}),
+    SCMD_ROSTER(item_unlock,      (cmdarg_t[2]){{"jid", pos_roster_itemlock_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_entity},{NULL}}),
+    SCMD_ROSTER(item_toggle_lock, (cmdarg_t[2]){{"jid", pos_roster_itemlock_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_entity},{NULL}}),
+    { "note", cmd_default, NULL, NULL,
+      (cmdopt_t[3]){
+        {'r', "reset", {"reset", pos_roster_note_rst, cmdarg_switch, NULL, NULL, NULL}},
+        {'j', "jid",   {"jid",   pos_roster_note_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_entity}},
+        {0}
+      },
+      (cmdarg_t[2]){
+        {"text", pos_roster_note_text, cmdarg_eol, NULL, &cmdarg_type_nonspace},
+        {NULL}
+      },
+      NULL, (gpointer)scmd_roster_note
+    },
+    SCMD_ROSTER(notes, NULL),
+    SCMD_ROSTER(resource_lock,   (cmdarg_t[2]){{"resource|fjid", pos_roster_reslock_jid, cmdarg_chreq, NULL, &cmdarg_type_buddy, (gpointer)cmdarg_roster_buddy},{NULL}}),
+    SCMD_ROSTER(resource_unlock, (cmdarg_t[2]){{"jid", pos_roster_reslock_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_buddy},{NULL}}),
+    SCMD_ROSTER(hide,   NULL),
+    SCMD_ROSTER(show,   NULL),
+    SCMD_ROSTER(toggle, NULL),
+    {NULL}
+  },
+};
+
+static gchar *do_roster (const cmdopts_t *options, cmdarg_value_t *values)
 {
-  int nbitems;
-
-  if (!nitems || !*nitems)
-    nbitems = 1;
-  else
-    nbitems = strtol(nitems, NULL, 10);
-
-  if (nbitems > 0)
-    scr_roster_up_down(updown, nbitems);
-}
-
-/* Commands callback functions */
-/* All these do_*() functions will be called with a "arg" parameter */
-/* (with arg not null)                                              */
-
-static void do_roster(char *arg)
-{
-  char **paramlst;
-  char *subcmd;
-
-  paramlst = split_arg(arg, 2, 1); // subcmd, arg
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  if (!strcasecmp(subcmd, "top")) {
+  scmd_roster_t subcmd = (scmd_roster_t) (values[pos_roster_scmd].value.cmd -> userdata);
+
+  if (subcmd == scmd_roster_bottom) {
+    scr_roster_bottom();
+    update_roster = TRUE;
+  } else if (subcmd == scmd_roster_top) {
     scr_roster_top();
     update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "bottom")) {
-    scr_roster_bottom();
+  } else if (subcmd == scmd_roster_up) {
+    scr_roster_up_down(-1, values[pos_roster_up_n].value.uint);
+  } else if (subcmd == scmd_roster_down) {
+    scr_roster_up_down(1, values[pos_roster_down_n].value.uint);
+  } else if (subcmd == scmd_roster_group_prev) {
+    scr_roster_prev_group();
+  } else if (subcmd == scmd_roster_group_next) {
+    scr_roster_next_group();
+  } else if (subcmd == scmd_roster_alternate) {
+    scr_roster_jump_alternate();
+  } else if (subcmd == scmd_roster_unread_first) {
+    scr_roster_unread_message(0);
+  } else if (subcmd == scmd_roster_unread_next) {
+    scr_roster_unread_message(1);
+  } else if (subcmd == scmd_roster_search) {
+    scr_roster_search(values[pos_roster_search_name].value.arg);
     update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "hide")) {
-    scr_roster_visibility(0);
-  } else if (!strcasecmp(subcmd, "show")) {
-    scr_roster_visibility(1);
-  } else if (!strcasecmp(subcmd, "toggle")) {
-    scr_roster_visibility(-1);
-  } else if (!strcasecmp(subcmd, "hide_offline")) {
+  } else if (subcmd == scmd_roster_display) {
+    scr_roster_display(values[pos_roster_display_mask].value.arg);
+  } else if (subcmd == scmd_roster_hide_offline) {
     buddylist_set_hide_offline_buddies(TRUE);
-    if (current_buddy)
+    if (current_buddy) // XXX
       buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "show_offline")) {
+  } else if (subcmd == scmd_roster_show_offline) {
     buddylist_set_hide_offline_buddies(FALSE);
     buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "toggle_offline")) {
+  } else if (subcmd == scmd_roster_toggle_offline) {
     buddylist_set_hide_offline_buddies(-1);
     buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "display")) {
-    scr_roster_display(arg);
-  } else if (!strcasecmp(subcmd, "item_lock")) {
-    roster_buddylock(arg, 1);
-  } else if (!strcasecmp(subcmd, "item_unlock")) {
-    roster_buddylock(arg, 0);
-  } else if (!strcasecmp(subcmd, "item_toggle_lock")) {
-    roster_buddylock(arg, -1);
-  } else if (!strcasecmp(subcmd, "unread_first")) {
-    scr_roster_unread_message(0);
-  } else if (!strcasecmp(subcmd, "unread_next")) {
-    scr_roster_unread_message(1);
-  } else if (!strcasecmp(subcmd, "alternate")) {
-    scr_roster_jump_alternate();
-  } else if (!strncasecmp(subcmd, "search", 6)) {
-    strip_arg_special_chars(arg);
-    if (!arg || !*arg) {
-      scr_LogPrint(LPRINT_NORMAL, "What name or JID are you looking for?");
-      free_arg_lst(paramlst);
-      return;
-    }
-    scr_roster_search(arg);
-    update_roster = TRUE;
-  } else if (!strcasecmp(subcmd, "up")) {
-    roster_updown(-1, arg);
-  } else if (!strcasecmp(subcmd, "down")) {
-    roster_updown(1, arg);
-  } else if (!strcasecmp(subcmd, "group_prev")) {
-    scr_roster_prev_group();
-  } else if (!strcasecmp(subcmd, "group_next")) {
-    scr_roster_next_group();
-  } else if (!strcasecmp(subcmd, "note")) {
-    roster_note(arg);
-  } else if (!strcasecmp(subcmd, "resource_lock")) {
-    roster_resourcelock(arg, TRUE);
-  } else if (!strcasecmp(subcmd, "resource_unlock")) {
-    roster_resourcelock(arg, FALSE);
-  } else
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-  free_arg_lst(paramlst);
+  } else if (subcmd == scmd_roster_item_lock) {
+    roster_buddylock(values[pos_roster_itemlock_jid].value.rjid.bud, 1);
+  } else if (subcmd == scmd_roster_item_unlock) {
+    roster_buddylock(values[pos_roster_itemlock_jid].value.rjid.bud, 0);
+  } else if (subcmd == scmd_roster_item_toggle_lock) {
+    roster_buddylock(values[pos_roster_itemlock_jid].value.rjid.bud, -1);
+  } else if (subcmd == scmd_roster_note) {
+    roster_note(values[pos_roster_note_jid].value.rjid.bud,
+                values[pos_roster_note_rst].value.swc,
+                values[pos_roster_note_text].value.arg);
+  } else if (subcmd == scmd_roster_notes) {
+    display_all_annotations();
+  } else if (subcmd == scmd_roster_resource_lock) {
+    buddy_setactiveresource(values[pos_roster_reslock_jid].value.rjid.bud,
+                            values[pos_roster_reslock_jid].value.rjid.resource);
+    scr_update_chat_status(TRUE);
+  } else if (subcmd == scmd_roster_resource_unlock) {
+    buddy_setactiveresource(values[pos_roster_reslock_jid].value.rjid.bud, NULL);
+    scr_update_chat_status(TRUE);
+  } else if (subcmd == scmd_roster_hide) {
+    scr_roster_visibility(0);
+  } else if (subcmd == scmd_roster_show) {
+    scr_roster_visibility(1);
+  } else { // scmd_roster_toggle
+    scr_roster_visibility(-1);
+  }
+
+  return NULL;
 }
 
-void do_color(char *arg)
+//
+//  /color
+//
+
+// custom argument types
+
+// statusmask + "clear"
+// Needs status char set in chkdata.
+static gchar *cmdarg_check_color_statusmask (cmdarg_value_t *arg)
 {
-  char **paramlst;
-  char *subcmd;
-
-  paramlst = split_arg(arg, 2, 1); // subcmd, arg
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  if (!strcasecmp(subcmd, "roster")) {
-    char *status, *wildcard, *color;
-    char **arglist = split_arg(arg, 3, 0);
-
-    status = *arglist;
-    wildcard = to_utf8(arglist[1]);
-    color = arglist[2];
-
-    if (status && !strcmp(status, "clear")) { // Not a color command, clear all
+  if (!g_strcmp0 (arg -> value.arg, "clear"))
+    return NULL;
+  else
+    return cmdarg_check_charset (arg);
+}
+
+static const cmdarg_type_t cmdarg_type_color_statusmask = {
+  cmdarg_check_color_statusmask,
+  NULL,
+  NULL,
+};
+
+// bjid + "*"
+static gchar *cmdarg_check_color_roomjid (cmdarg_value_t *arg)
+{
+  if (!g_strcmp0 (arg -> value.arg, "*"))
+    return NULL;
+  else
+    return cmdarg_check_bjid (arg);
+}
+
+static const cmdarg_type_t cmdarg_type_color_roomjid = {
+  cmdarg_check_color_roomjid,
+  NULL,
+  NULL,
+};
+
+// TODO: completion, that says, which roster jids do match
+static const cmdarg_type_t cmdarg_type_bjidmask = {
+  cmdarg_check_nonspace,
+  NULL,
+  NULL,
+};
+
+// TODO: completion, based on current room nicks
+static const cmdarg_type_t cmdarg_type_nick = {
+  cmdarg_check_nonspace,
+  NULL,
+  NULL,
+};
+
+// command
+
+static gchar *do_color (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  scmd_color_roster,
+  scmd_color_mucnick,
+  scmd_color_muc,
+} scmd_color_t;
+
+typedef enum {
+  pos_color_scmd          = 0,
+  pos_color_roster_status = 1,
+  pos_color_roster_jid    = 2,
+  pos_color_roster_color  = 3,
+  pos_color_muc_room      = 1,
+  pos_color_muc_mode      = 2,
+  pos_color_nick_nick     = 1,
+  pos_color_nick_color    = 2,
+} pos_color_t;
+
+static const string2enum_t s2e_color_muc[] = {
+  { "on",     MC_ALL    },
+  { "off",    MC_OFF    },
+  { "preset", MC_PRESET },
+  { "-",      MC_REMOVE },
+  { NULL,     0         },
+};
+
+static cmdopts_t def_color = {
+  "color",
+  cmd_safe,
+  NULL,
+  do_color,
+  NULL,
+  (cmdarg_t[2]){{ "subcommand", pos_color_scmd, cmdarg_subcmd | cmdarg_chreq, NULL, NULL },{NULL}},
+  (cmdopts_t[4]){
+    {"roster", cmd_default, NULL, NULL, NULL, (cmdarg_t[4]){
+        { "statusmask|clear", pos_color_roster_status, cmdarg_chreq, NULL, &cmdarg_type_color_statusmask, (gpointer)"ofdna_?" },
+        { "jidmask",          pos_color_roster_jid,    cmdarg_check, NULL, &cmdarg_type_bjidmask },
+        { "color|-",          pos_color_roster_color,  cmdarg_check, NULL, &cmdarg_type_color },
+        {NULL}
+      }, NULL, (gpointer)scmd_color_roster},
+    {"muc", cmd_default, NULL, NULL, NULL, (cmdarg_t[3]){
+        { "roomjid",         pos_color_muc_room, cmdarg_chreq, NULL, &cmdarg_type_color_roomjid, (gpointer)cmdarg_roster_room },
+        { "on|off|preset|-", pos_color_muc_mode, cmdarg_chreq, "on", &cmdarg_type_string2enum, (gpointer)s2e_color_muc },
+        {NULL}
+      }, NULL, (gpointer)scmd_color_muc},
+    {"mucnick", cmd_default, NULL, NULL, NULL, (cmdarg_t[3]){
+        { "nick",    pos_color_nick_nick,  cmdarg_chreq, NULL, &cmdarg_type_nick },
+        { "color|-", pos_color_nick_color, cmdarg_chreq, NULL, &cmdarg_type_color },
+        {NULL}
+      }, NULL, (gpointer)scmd_color_mucnick},
+    {NULL}
+  },
+};
+
+static gchar *do_color (const cmdopts_t *options, cmdarg_value_t *values)
+{
+  scmd_color_t subcmd = (scmd_color_t) (values[pos_color_scmd].value.cmd -> userdata);
+
+  if (subcmd == scmd_color_roster) {
+    const gchar *status   = values[pos_color_roster_status].value.arg;
+    const gchar *wildcard = values[pos_color_roster_jid].value.arg;
+    const gchar *color    = values[pos_color_roster_color].value.arg;
+    if (!strcmp(status, "clear")) { // Not a color command, clear all
       scr_roster_clear_color();
       update_roster = TRUE;
     } else {
-      if (!status || !*status || !wildcard || !*wildcard || !color || !*color) {
-        scr_LogPrint(LPRINT_NORMAL, "Missing argument");
+      if ((!wildcard) || (!color)) {
+        // freaking "clear" :(
+        return g_strdup ("Missing argument.");
       } else {
         update_roster = scr_roster_color(status, wildcard, color) ||
                         update_roster;
       }
     }
-    free_arg_lst(arglist);
-    g_free(wildcard);
-  } else if (!strcasecmp(subcmd, "muc")) {
-    char **arglist = split_arg(arg, 2, 0);
-    char *free_muc = to_utf8(*arglist);
-    const char *muc = free_muc, *mode = arglist[1];
-    if (!muc || !*muc)
-      scr_LogPrint(LPRINT_NORMAL, "What MUC?");
-    else {
-      if (!strcmp(muc, "."))
-        if (!(muc = CURRENT_JID))
-          scr_LogPrint(LPRINT_NORMAL, "No JID selected");
-      if (muc) {
-        if (check_jid_syntax(muc) && strcmp(muc, "*"))
-          scr_LogPrint(LPRINT_NORMAL, "Not a JID");
-        else {
-          if (!mode || !*mode || !strcasecmp(mode, "on"))
-            scr_muc_color(muc, MC_ALL);
-          else if (!strcasecmp(mode, "preset"))
-            scr_muc_color(muc, MC_PRESET);
-          else if (!strcasecmp(mode, "off"))
-            scr_muc_color(muc, MC_OFF);
-          else if (!strcmp(mode, "-"))
-            scr_muc_color(muc, MC_REMOVE);
-          else
-            scr_LogPrint(LPRINT_NORMAL, "Unknown coloring mode");
-        }
-      }
-    }
-    free_arg_lst(arglist);
-    g_free(free_muc);
-  } else if (!strcasecmp(subcmd, "mucnick")) {
-    char **arglist = split_arg(arg, 2, 0);
-    const char *nick = *arglist, *color = arglist[1];
-    if (!nick || !*nick || !color || !*color)
-      scr_LogPrint(LPRINT_NORMAL, "Missing argument");
-    else
-      scr_muc_nick_color(nick, color);
-    free_arg_lst(arglist);
-  } else
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-  free_arg_lst(paramlst);
+  } else if (subcmd == scmd_color_muc) {
+    scr_muc_color ( values[pos_color_muc_room].value.arg,
+                    values[pos_color_muc_mode].value.uint );
+  } else { // scmd_color_mucnick
+    scr_muc_nick_color( values[pos_color_nick_nick].value.arg,
+                        values[pos_color_nick_color].value.arg );
+  }
+
+  return NULL;
 }
 
-//  cmd_setstatus(recipient, arg)
-// Set your Jabber status.
-// - if recipient is not NULL, the status is sent to this contact only
-// - arg must be "status message" (message is optional)
-void cmd_setstatus(const char *recipient, const char *arg)
+//
+//  /status
+//
+
+// custom type
+
+// needs corresponding s2e in chkdata
+static gchar *cmdarg_check_status_status (cmdarg_value_t *arg)
 {
-  char **paramlst;
-  char *status;
-  char *msg;
-  enum imstatus st;
-
-  if (!xmpp_is_online())
-    scr_LogPrint(LPRINT_NORMAL, "You are currently not connected...");
-  // We do not return now, so that the status is memorized and used later...
-
-  // It makes sense to reset autoaway before changing the status
-  // (esp. for FIFO or remote commands) or the behaviour could be
-  // unexpected...
-  if (!recipient)
-    scr_check_auto_away(TRUE);
-
-  paramlst = split_arg(arg, 2, 1); // status, message
-  status = *paramlst;
-  msg = *(paramlst+1);
-
-  if (!status) {
-    free_arg_lst(paramlst);
-    return;
+  if (!g_strcmp0(arg -> value.arg, "message")) {
+    arg -> value.uint = xmpp_getstatus(); // Preserve current status
+    return NULL;
   }
 
-  if      (!strcasecmp(status, IMSTATUS_OFFLINE))       st = offline;
-  else if (!strcasecmp(status, IMSTATUS_ONLINE))        st = available;
-  else if (!strcasecmp(status, IMSTATUS_AVAILABLE))     st = available;
-  else if (!strcasecmp(status, IMSTATUS_AWAY))          st = away;
+  return cmdarg_check_string2enum (arg);
+}
+
+static const cmdarg_type_t cmdarg_type_status_status = {
+  cmdarg_check_status_status,
+  NULL,
+  NULL,
+};
+
+// command
+
+static gchar *do_status (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_status_status  = 0,
+  pos_status_jid     = 1,
+  pos_status_message = 2,
+} pos_status_t;
+
+static const string2enum_t s2e_status2[] = {
+  { "away",      away        },
+  { "offline",   offline     },
+  { "online",    available   },
+  { "avail",     available   },
+  { "notavail",  notavail    },
+  { "dnd",       dontdisturb },
+  { "free",      freeforchat },
 #ifdef WITH_DEPRECATED_STATUS_INVISIBLE
-  else if (!strcasecmp(status, IMSTATUS_INVISIBLE))     st = invisible;
+  { "invisible", invisible   },
 #endif
-  else if (!strcasecmp(status, IMSTATUS_DONOTDISTURB))  st = dontdisturb;
-  else if (!strcasecmp(status, IMSTATUS_NOTAVAILABLE))  st = notavail;
-  else if (!strcasecmp(status, IMSTATUS_FREE4CHAT))     st = freeforchat;
-  else if (!strcasecmp(status, "message")) {
-    if (!msg || !*msg) {
-      // We want a message.  If there's none, we give up.
-      scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-      free_arg_lst(paramlst);
-      return;
-    }
-    st = xmpp_getstatus();  // Preserve current status
-  } else {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized status!");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  // Use provided message
-  if (msg && !*msg) {
-    msg = NULL;
-  }
-
-  // If a recipient is specified, let's don't use default status messages
-  if (recipient && !msg)
-    msg = "";
-
-  xmpp_setstatus(st, recipient, msg, FALSE);
-
-  free_arg_lst(paramlst);
-}
-
-static void do_status(char *arg)
+  // XXX ugly
+  { "show",      imstatus_size},
+  { NULL,        0           },
+};
+
+static cmdopts_t def_status = {
+  "status",
+  cmd_safe,
+  NULL,
+  do_status,
+  (cmdopt_t[2]){
+    { 't', "to",
+           { "jid", pos_status_jid, cmdarg_default, NULL, &cmdarg_type_fjid }, (gpointer)cmdarg_roster_entity },
+    {0}
+  },
+  (cmdarg_t[3]){
+    { "status",  pos_status_status,  cmdarg_chreq, "show",
+                 &cmdarg_type_status_status, (gpointer)s2e_status2 },
+    { "message", pos_status_message, cmdarg_eol,   NULL,
+                 &cmdarg_type_nonspace },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_status (const cmdopts_t *options, cmdarg_value_t *values)
 {
-  if (!*arg) {
+  if (values[pos_status_status].value.uint == imstatus_size) {
     const char *sm = xmpp_getstatusmsg();
     scr_LogPrint(LPRINT_NORMAL, "Your status is: [%c] %s",
                  imstatus2char[xmpp_getstatus()],
                  (sm ? sm : ""));
-    return;
+  } else {
+    if (!xmpp_is_online())
+      scr_LogPrint(LPRINT_NORMAL, "You are currently not connected...");
+    scr_check_auto_away(TRUE);
+    xmpp_setstatus(values[pos_status_status].value.uint,
+                   values[pos_status_jid].value.arg,
+                   values[pos_status_message].value.arg, FALSE);
   }
-  arg = to_utf8(arg);
-  cmd_setstatus(NULL, arg);
-  g_free(arg);
+  return NULL;
 }
 
-static void do_status_to(char *arg)
+//
+//  /status_to
+//
+
+static gchar *do_status_to (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_statusto_jid     = 0,
+  pos_statusto_status  = 1,
+  pos_statusto_message = 2,
+} pos_statusto_t;
+
+// no "show" here
+static const string2enum_t s2e_status[] = {
+  { "away",      away        },
+  { "offline",   offline     },
+  { "online",    available   },
+  { "avail",     available   },
+  { "notavail",  notavail    },
+  { "dnd",       dontdisturb },
+  { "free",      freeforchat },
+#ifdef WITH_DEPRECATED_STATUS_INVISIBLE
+  { "invisible", invisible   },
+#endif
+  { NULL,        0           },
+};
+
+static cmdopts_t def_status_to = {
+  "status_to",
+  cmd_default,
+  NULL,
+  do_status_to,
+  NULL,
+  (cmdarg_t[4]){
+    {"jid",     pos_statusto_jid,     cmdarg_chreq, NULL, &cmdarg_type_fjid, (gpointer)cmdarg_roster_entity},
+    {"status",  pos_statusto_status,  cmdarg_chreq, NULL, &cmdarg_type_status_status, (gpointer)s2e_status},
+    {"message", pos_statusto_message, cmdarg_eol,   NULL, &cmdarg_type_nonspace},
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_status_to (const cmdopts_t *options, cmdarg_value_t *values)
 {
-  char **paramlst;
-  char *fjid, *st, *msg;
-  char *jid_utf8 = NULL;
-
-  paramlst = split_arg(arg, 3, 1); // jid, status, [message]
-  fjid = *paramlst;
-  st = *(paramlst+1);
-  msg = *(paramlst+2);
-
-  if (!fjid || !st) {
-    scr_LogPrint(LPRINT_NORMAL,
-                 "Please specify both a Jabber ID and a status.");
-    free_arg_lst(paramlst);
-    return;
+  const char *fjid, *stname, *msg;
+  enum imstatus st;
+
+  fjid = values[pos_statusto_jid].value.arg;
+  st   = values[pos_statusto_status].value.uint;
+  msg  = values[pos_statusto_message].value.arg;
+
+  { // get status name
+    const string2enum_t *list;
+    for (list = s2e_status; list -> name != NULL; list ++)
+      if (list -> value == st) {
+        stname = list -> name;
+        break;
+      }
   }
-
-  // Allow things like /status_to "" away
-  if (!*fjid || !strcmp(fjid, "."))
-    fjid = NULL;
-
-  if (fjid) {
-    // The JID has been specified.  Quick check...
-    if (check_jid_syntax(fjid)) {
-      scr_LogPrint(LPRINT_NORMAL|LPRINT_NOTUTF8,
-                   "<%s> is not a valid Jabber ID.", fjid);
-      fjid = NULL;
-    } else {
-      // Convert jid to lowercase
-      char *p = fjid;
-      for ( ; *p && *p != JID_RESOURCE_SEPARATOR; p++)
-        *p = tolower(*p);
-      fjid = jid_utf8 = to_utf8(fjid);
-    }
+  // prevent default status message
+  msg = msg ? msg : "";
+
+  scr_log_print (LPRINT_LOGNORM, 
+                 "Sending to <%s> /status %s %s", fjid, stname, msg);
+  if (!xmpp_is_online())
+    scr_log_print (LPRINT_NORMAL, "You are currently not connected...");
+  xmpp_setstatus(st, fjid, msg, FALSE);
+
+  return NULL;
+}
+
+//
+//  /add
+//
+
+static gchar *do_add (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_add_jid  = 0,
+  pos_add_name = 1,
+} pos_add_t;
+
+static cmdopts_t def_add = {
+  "add",
+  cmd_default,
+  cmd_check_online,
+  do_add,
+  NULL,
+  (cmdarg_t[3]){
+    { "jid",  pos_add_jid,  cmdarg_chreq,   ".",  &cmdarg_type_bjid, (gpointer)cmdarg_roster_entity },
+    { "name", pos_add_name, cmdarg_default, NULL, &cmdarg_type_nonspace },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_add (const cmdopts_t *options, cmdarg_value_t *values)
+{
+  gchar *jid = values[pos_add_jid].value.arg;
+
+  // XXX
+  //mc_strtolower(jid);
+
+  xmpp_addbuddy(jid, values[pos_add_name].value.arg, NULL);
+  scr_log_print (LPRINT_LOGNORM, "Sent presence notification request to <%s>.",
+                 jid);
+
+  return NULL;
+}
+
+//
+//  /del
+//
+
+static gchar *do_del (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_del_jid    = 0,
+  pos_del_dryrun = 1,
+} pos_del_t;
+
+static cmdopts_t def_del = {
+  "del",
+  cmd_default,
+  cmd_check_online,
+  do_del,
+  (cmdopt_t[2]){
+    { 'n', "dryrun",
+      { "dryrun", pos_del_dryrun, cmdarg_trigger, NULL, NULL } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "jid", pos_del_jid, cmdarg_chreq, ".", &cmdarg_type_buddy, (gpointer)cmdarg_roster_entity },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_del (const cmdopts_t *options, cmdarg_value_t *values)
+{
+  gpointer   buddy = values[pos_del_jid].value.rjid.bud;
+  const char *jid  = buddy_getjid (buddy);
+
+  if (buddy_gettype(buddy) & ROSTER_TYPE_ROOM)
+    // This is a chatroom
+    if (buddy_getinsideroom(buddy))
+      return g_strdup_printf ("You have to leave room <%s> before deleting it!",
+                              jid);
+
+  scr_log_print (LPRINT_LOGNORM, "Removing <%s>...", jid);
+
+  if (values[pos_del_dryrun].value.swc) {
+    scr_log_print (LPRINT_LOGNORM, "... not really.");
   } else {
-    // Add the current buddy
-    if (current_buddy)
-      fjid = (char*)buddy_getjid(BUDDATA(current_buddy));
-    if (!fjid)
-      scr_LogPrint(LPRINT_NORMAL, "Please specify a Jabber ID.");
+    // Close the buffer
+    scr_buffer_purge(1, jid);
+
+    xmpp_delbuddy(jid);
+    scr_update_buddy_window();
   }
 
-  if (fjid) {
-    char *cmdline;
-    if (!msg)
-      msg = "";
-    msg = to_utf8(msg);
-    cmdline = g_strdup_printf("%s %s", st, msg);
-    scr_LogPrint(LPRINT_LOGNORM, "Sending to <%s> /status %s", fjid, cmdline);
-    cmd_setstatus(fjid, cmdline);
-    g_free(msg);
-    g_free(cmdline);
-    g_free(jid_utf8);
-  }
-  free_arg_lst(paramlst);
+  return NULL;
 }
 
-static void do_add(char *arg)
+//
+//  /group
+//
+
+static void group_cmd (gpointer group, scmd_group_t action) 
 {
-  char **paramlst;
-  char *id, *nick;
-  char *jid_utf8 = NULL;
-
-  if (!xmpp_is_online()) {
-    scr_LogPrint(LPRINT_NORMAL, "You are not connected.");
-    return;
-  }
-
-  paramlst = split_arg(arg, 2, 0); // jid, [nickname]
-  id = *paramlst;
-  nick = *(paramlst+1);
-
-  if (!id)
-    nick = NULL; // Allow things like: /add "" nick
-  else if (!*id || !strcmp(id, "."))
-    id = NULL;
-
-  if (id) {
-    // The JID has been specified.  Quick check...
-    if (check_jid_syntax(id)) {
-      scr_LogPrint(LPRINT_NORMAL|LPRINT_NOTUTF8,
-                   "<%s> is not a valid Jabber ID.", id);
-      id = NULL;
-    } else {
-      mc_strtolower(id);
-      id = jid_utf8 = to_utf8(id);
-    }
-  } else {
-    // Add the current buddy
-    if (current_buddy)
-      id = (char*)buddy_getjid(BUDDATA(current_buddy));
-    if (!id)
-      scr_LogPrint(LPRINT_NORMAL, "Please specify a Jabber ID.");
-  }
-
-  if (nick)
-    nick = to_utf8(nick);
-
-  if (id) {
-    // 2nd parameter = optional nickname
-    xmpp_addbuddy(id, nick, NULL);
-    scr_LogPrint(LPRINT_LOGNORM, "Sent presence notification request to <%s>.",
-                 id);
-  }
-
-  g_free(jid_utf8);
-  g_free(nick);
-  free_arg_lst(paramlst);
-}
-
-static void do_del(char *arg)
-{
-  const char *bjid;
-
-  if (*arg) {
-    scr_LogPrint(LPRINT_NORMAL, "This action does not require a parameter; "
-                 "the currently-selected buddy will be deleted.");
-    return;
-  }
-
-  if (!current_buddy)
-    return;
-  bjid = buddy_getjid(BUDDATA(current_buddy));
-  if (!bjid)
-    return;
-
-  if (buddy_gettype(BUDDATA(current_buddy)) & ROSTER_TYPE_ROOM) {
-    // This is a chatroom
-    if (buddy_getinsideroom(BUDDATA(current_buddy))) {
-      scr_LogPrint(LPRINT_NORMAL, "You haven't left this room!");
-      return;
-    }
-  }
-
-  // Close the buffer
-  scr_buffer_purge(1, NULL);
-
-  scr_LogPrint(LPRINT_LOGNORM, "Removing <%s>...", bjid);
-  xmpp_delbuddy(bjid);
-  scr_update_buddy_window();
-}
-
-static void do_group(char *arg)
-{
-  gpointer group = NULL;
-  guint leave_buddywindow;
-  char **paramlst;
-  char *subcmd;
-  enum { group_toggle = -1, group_unfold = 0, group_fold = 1 } group_state = 0;
-
-  if (!*arg) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    return;
-  }
-
-  if (!current_buddy)
-    return;
-
-  paramlst = split_arg(arg, 2, 0); // subcmd, [arg]
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd)
-    goto do_group_return;   // Should not happen
-
-  if (!strcasecmp(subcmd, "expand") || !strcasecmp(subcmd, "unfold"))
-    group_state = group_unfold;
-  else if (!strcasecmp(subcmd, "shrink") || !strcasecmp(subcmd, "fold"))
-    group_state = group_fold;
-  else if (!strcasecmp(subcmd, "toggle"))
-    group_state = group_toggle;
-  else {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-    goto do_group_return;
-  }
-
-  if (arg && *arg) {
-    GSList *roster_elt;
-    char *group_utf8 = to_utf8(arg);
-    roster_elt = roster_find(group_utf8, namesearch, ROSTER_TYPE_GROUP);
-    g_free(group_utf8);
-    if (roster_elt)
-      group = buddy_getgroup(roster_elt->data);
-  } else {
-    group = buddy_getgroup(BUDDATA(current_buddy));
-  }
-  if (!group) {
-    scr_LogPrint(LPRINT_NORMAL, "Group not found.");
-    goto do_group_return;
-  }
-
   // We'll have to redraw the chat window if we're not currently on the group
   // entry itself, because it means we'll have to leave the current buddy
   // chat window.
-  leave_buddywindow = (group != BUDDATA(current_buddy) &&
-                       group == buddy_getgroup(BUDDATA(current_buddy)));
-
-  if (!(buddy_gettype(group) & ROSTER_TYPE_GROUP)) {
-    scr_LogPrint(LPRINT_NORMAL, "You need to select a group.");
-    goto do_group_return;
-  }
-
-  if (group_state != group_unfold && leave_buddywindow)
+  if (action != scmd_group_unfold &&
+      ((!current_buddy) ||
+       (group != BUDDATA(current_buddy) &&
+        group == buddy_getgroup(BUDDATA(current_buddy)))))
     scr_roster_prev_group();
 
-  buddy_hide_group(group, group_state);
+  buddy_hide_group(group, action);
 
   buddylist_build();
   update_roster = TRUE;
-
-do_group_return:
-  free_arg_lst(paramlst);
 }
 
+static gchar *do_group (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_group_group  = 0,
+  pos_group_action = 1,
+} pos_group_t;
+
+static const string2enum_t s2e_group_scmd[] = {
+  { "expand", scmd_group_unfold },
+  { "unfold", scmd_group_unfold },
+  { "shrink", scmd_group_fold   },
+  { "fold",   scmd_group_fold   },
+  { "toggle", scmd_group_toggle },
+  { NULL,     0                 },
+};
+
+#define SCMD_GROUP(NAME, REALNAME) \
+    { #NAME, cmd_default, NULL, NULL, NULL, NULL, NULL, \
+             (gpointer)scmd_group_##REALNAME }
+static cmdopts_t def_group = {
+  "group",
+  cmd_default,
+  NULL,
+  do_group,
+  NULL,
+  (cmdarg_t[3]){
+    { "subcommand", pos_group_action, cmdarg_chreq,              NULL,
+                    &cmdarg_type_string2enum, (gpointer)s2e_group_scmd },
+    { "group",      pos_group_group,  cmdarg_chreq | cmdarg_eol, ".",
+                    &cmdarg_type_buddy, (gpointer)cmdarg_roster_grouponly },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_group(const cmdopts_t *options, cmdarg_value_t *values)
+{
+  //if (!current_buddy) // XXX do we need this still?
+  //  return g_strdup("Command needs selected buddy.");
+
+  group_cmd (values[pos_group_group].value.rjid.bud,
+             values[pos_group_action].value.uint);
+
+  return NULL;
+}
+
+//
+//  /say
+//
+
+// FIXME: remove excessive checks, simplify this all. Uses:
+// send_message_to():
+// - send_message()
+// - /msay send_to
+// - /say_to
+// - /room privmsg
+// send_message():
+// - say_cmd()
+// - /msay send
+// say_cmd():
+// - process_line()
+// - /say
+// Probably at least send_message can be removed, and /msay send can use
+// say_cmd().
+// 
+// say_cmd() does:
+// - scr_set_chatmode(TRUE);
+// - scr_show_buddy_window();
+// - check current buddy presence and type
+// - buddy_setflags(bud, ROSTER_FLAG_LOCK, TRUE);
+// send_message() does:
+// - check current buddy presence and jid availability (type check should ensure that, I think)
+// - determines active resource of current buddy
+// send_message_to() does:
+// - checks online
+// - checks jid presence
+// - checks message presence
+// - checks syntax of jid
+// - converts message type
+// - extracts bare jid (for hook)
+// - jumps to window if not quiet
+// - unsets resource (for hook)
+// - prepares message for hook
+// - sends message
+// - checks for encryption error
+// - calls hook
+// So, we can make 'activeresource' checker, that will eliminate most of the checks,
+// we can move online check to caller space, we can do chatmode things only when not
+// quiet. That essentially leaves only send_message_to. However that causes mcabber
+// not enter chat mode, when on wrong buddy (status/group).
+// Add '--to' and '--subject' to commands.
+
 static int send_message_to(const char *fjid, const char *msg, const char *subj,
-                           LmMessageSubType type_overwrite, bool quiet)
+                           msgtype_t msg_type, bool quiet)
 {
   char *bare_jid, *rp;
   char *hmsg;
@@ -1285,6 +2169,7 @@
   gint retval = 0;
   int isroom;
   gpointer xep184 = NULL;
+  LmMessageSubType type_overwrite = LM_MESSAGE_SUB_TYPE_NOT_SET;
 
   if (!xmpp_is_online()) {
     scr_LogPrint(LPRINT_NORMAL, "You are not connected.");
@@ -1299,11 +2184,15 @@
     return 1;
   }
   if (check_jid_syntax((char*)fjid)) {
-    scr_LogPrint(LPRINT_NORMAL|LPRINT_NOTUTF8,
-                 "<%s> is not a valid Jabber ID.", fjid);
+    scr_LogPrint(LPRINT_NORMAL, "<%s> is not a valid Jabber ID.", fjid);
     return 1;
   }
 
+  if (msg_type == msgtype_normal)
+    type_overwrite = LM_MESSAGE_SUB_TYPE_NORMAL;
+  else if (msg_type == msgtype_headline)
+    type_overwrite = LM_MESSAGE_SUB_TYPE_HEADLINE;
+
   // We must use the bare jid in hk_message_out()
   rp = strchr(fjid, JID_RESOURCE_SEPARATOR);
   if (rp)
@@ -1351,213 +2240,221 @@
   return retval;
 }
 
-//  send_message(msg, subj, type_overwrite)
-// Write the message in the buddy's window and send the message on
-// the network.
-static void send_message(const char *msg, const char *subj,
-                         LmMessageSubType type_overwrite)
+static void say_cmd(char *arg)
 {
-  const char *bjid;
-  char *jid;
-  const char *activeres;
-
+  // it is current buddy
   if (!current_buddy) {
-    scr_LogPrint(LPRINT_NORMAL, "No buddy is currently selected.");
+    scr_log_print (LPRINT_NORMAL, "No buddy selected.");
+    return;
+  } else if (!(buddy_gettype (BUDDATA(current_buddy)) & cmdarg_roster_entity)) {
+    scr_log_print (LPRINT_NORMAL, "Currently selected buddy is of wrong type.");
     return;
   }
 
-  bjid = CURRENT_JID;
-  if (!bjid) {
-    scr_LogPrint(LPRINT_NORMAL, "No buddy is currently selected.");
-    return;
-  }
-
-  activeres = buddy_getactiveresource(BUDDATA(current_buddy));
-  if (activeres)
-    jid = g_strdup_printf("%s/%s", bjid, activeres);
-  else
-    jid = g_strdup(bjid);
-
-  send_message_to(jid, msg, subj, type_overwrite, FALSE);
-  g_free(jid);
-}
-
-static LmMessageSubType scan_mtype(char **arg)
-{
-  // Try splitting it
-  char **parlist = split_arg(*arg, 2, 1);
-  LmMessageSubType result = LM_MESSAGE_SUB_TYPE_NOT_SET;
-  // Is it a good parameter?
-  if (parlist && *parlist) {
-    if (!strcmp("-n", *parlist)) {
-      result = LM_MESSAGE_SUB_TYPE_NORMAL;
-    } else if (!strcmp("-h", *parlist)) {
-      result = LM_MESSAGE_SUB_TYPE_HEADLINE;
-    }
-    if (result != LM_MESSAGE_SUB_TYPE_NOT_SET || (!strcmp("--", *parlist)))
-      *arg += strlen(*arg) - (parlist[1] ? strlen(parlist[1]) : 0);
-  }
-  // Anything found? -> skip it
-  free_arg_lst(parlist);
-  return result;
-}
-
-void say_cmd(char *arg, int parse_flags)
-{
-  gpointer bud;
-  LmMessageSubType msgtype = LM_MESSAGE_SUB_TYPE_NOT_SET;
-
   scr_set_chatmode(TRUE);
   scr_show_buddy_window();
 
-  if (!current_buddy) {
-    scr_LogPrint(LPRINT_NORMAL,
-                 "Whom are you talking to?  Please select a buddy.");
-    return;
+  buddy_setflags(bud, ROSTER_FLAG_LOCK, TRUE);
+
+  const char *resource = buddy_getactiveresource (BUDDATA(current_buddy));
+  const char *jid      = buddy_getjid (BUDDATA(current_buddy));
+  gchar      *fjid     = g_strdup_printf ("%s" JID_RESOURCE_SEPARATORSTR "%s", jid, resource);
+
+  send_message_to (fjid, arg, NULL, msgtype_not_set, FALSE);
+  g_free (fjid);
+}
+
+static gchar *do_say (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_say_msg     = 0,
+  pos_say_msgtype = 1,
+} pos_say_t;
+
+static cmdopts_t def_say = {
+  "say",
+  cmd_default,
+  NULL,
+  do_say,
+  (cmdopt_t[4]){
+    { 'n', "normal",   { "normal",   pos_say_msgtype, cmdarg_switch, NULL, NULL,
+                                     NULL, (gpointer)msgtype_normal}},
+    { 'h', "headline", { "headline", pos_say_msgtype, cmdarg_switch, NULL, NULL,
+                                     NULL, (gpointer)msgtype_headline } },
+    { 'd', "default",  { "default",  pos_say_msgtype, cmdarg_switch, NULL, NULL,
+                                     NULL, (gpointer)msgtype_not_set } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "message", pos_say_msg, cmdarg_eol | cmdarg_chreq, NULL,
+                                     &cmdarg_type_nonspace },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_say (const cmdopts_t *options, cmdarg_value_t *values)
+{
+  say_cmd(values[pos_say_msg].value.arg,
+          (msgtype_t) (values[pos_say_msgtype].src -> userdata));
+  return NULL;
+}
+
+//
+//  /msay
+//
+
+static gchar *do_msay (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  scmd_msay_begin, scmd_msay_verbatim,
+  scmd_msay_send, scmd_msay_send_to,
+  scmd_msay_toggle, scmd_msay_toggle_verbatim,
+  scmd_msay_abort,
+} scmd_msay_t;
+
+typedef enum {
+  pos_msay_scmd    = 0,
+  pos_msay_subject = 1,
+  pos_msay_jid     = 1,
+  pos_msay_msgtype = 2,
+} pos_msay_t;
+
+static cmdopts_t def_msay = {
+  "msay",
+  cmd_default,
+  NULL,
+  do_msay,
+  NULL,
+  (cmdarg_t[2]){
+    { "subcommand", pos_msay_scmd, cmdarg_subcmd|cmdarg_chreq, NULL, NULL },
+    {NULL},
+  },
+  (cmdopts_t[8]){
+    { "begin", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){{"subject", pos_msay_subject, cmdarg_eol, NULL, &cmdarg_type_nonspace}, {NULL}},
+      NULL, (gpointer)scmd_msay_begin },
+    { "verbatim", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){{"subject", pos_msay_subject, cmdarg_eol, NULL, &cmdarg_type_nonspace}, {NULL}},
+      NULL, (gpointer)scmd_msay_verbatim },
+    { "send", cmd_default, NULL, NULL,
+      (cmdopt_t[4]){
+        {'n', "normal",   {"normal",   pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_normal}},
+        {'h', "headline", {"headline", pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_headline}},
+        {'d', "default",  {"default",  pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_not_set}},
+        {0}
+      },
+      NULL, NULL, (gpointer)scmd_msay_send, 0 },
+    { "send_to", cmd_default, NULL, NULL,
+      (cmdopt_t[4]){
+        {'n', "normal",   {"normal",   pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_normal}},
+        {'h', "headline", {"headline", pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_headline}},
+        {'d', "default",  {"default",  pos_msay_msgtype, cmdarg_switch, NULL, NULL, NULL, (gpointer)msgtype_not_set}},
+        {0}
+      },
+      (cmdarg_t[2]){{"jid", pos_msay_jid, cmdarg_chreq, NULL, &cmdarg_type_fjid, (gpointer)cmdarg_roster_entity}, {NULL}}, 
+      NULL, (gpointer)scmd_msay_send_to },
+    { "toggle", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_msay_toggle },
+    { "toggle_verbatim", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_msay_toggle_verbatim },
+    { "abort", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_msay_abort },
+  },
+};
+
+static gchar *do_msay (const cmdopts_t *command, cmdarg_value_t *values)
+{
+  const char  *msg;
+  scmd_msay_t subcmd = (scmd_msay_t) (values[pos_msay_scmd].src -> userdata);
+
+  if (subcmd == scmd_msay_toggle) {
+    if (scr_get_multimode())
+      subcmd = scmd_msay_send;
+    else
+      subcmd = scmd_msay_begin;
+  } else if (subcmd == scmd_msay_toggle_verbatim) {
+    if (scr_get_multimode())
+      subcmd = scmd_msay_send;
+    else
+      subcmd = scmd_msay_verbatim;
   }
 
-  bud = BUDDATA(current_buddy);
-  if (!(buddy_gettype(bud) &
-        (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM))) {
-    scr_LogPrint(LPRINT_NORMAL, "This is not a user.");
-    return;
-  }
-
-  buddy_setflags(bud, ROSTER_FLAG_LOCK, TRUE);
-  if (parse_flags)
-    msgtype = scan_mtype(&arg);
-  arg = to_utf8(arg);
-  send_message(arg, NULL, msgtype);
-  g_free(arg);
-}
-
-static void do_say(char *arg) {
-  say_cmd(arg, 1);
-}
-
-static void do_msay(char *arg)
-{
-  /* Parameters: begin verbatim abort send send_to */
-  char **paramlst;
-  char *subcmd;
-
-  paramlst = split_arg(arg, 2, 1); // subcmd, arg
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    scr_LogPrint(LPRINT_NORMAL, "Please read the manual before using "
-                 "the /msay command.");
-    scr_LogPrint(LPRINT_NORMAL, "(Use \"%s begin\" to enter "
-                 "multi-line mode...)", mkcmdstr("msay"));
-    goto do_msay_return;
-  }
-
-  if (!strcasecmp(subcmd, "toggle")) {
-    if (scr_get_multimode())
-      subcmd = "send";
-    else
-      subcmd = "begin";
-  } else if (!strcasecmp(subcmd, "toggle_verbatim")) {
-    if (scr_get_multimode())
-      subcmd = "send";
-    else
-      subcmd = "verbatim";
-  }
-
-  if (!strcasecmp(subcmd, "abort")) {
+  if (subcmd == scmd_msay_abort) {
     if (scr_get_multimode())
       scr_LogPrint(LPRINT_NORMAL, "Leaving multi-line message mode.");
     scr_set_multimode(FALSE, NULL);
-    goto do_msay_return;
-  } else if ((!strcasecmp(subcmd, "begin")) ||
-             (!strcasecmp(subcmd, "verbatim"))) {
-    bool verbat;
-    gchar *subj_utf8 = to_utf8(arg);
-    if (!strcasecmp(subcmd, "verbatim")) {
-      scr_set_multimode(2, subj_utf8);
-      verbat = TRUE;
+    return NULL;
+  } else if (subcmd == scmd_msay_begin || subcmd == scmd_msay_verbatim) {
+    gchar *subject = values[pos_msay_subject].value.arg;
+
+    if (subcmd == scmd_msay_verbatim) {
+      scr_set_multimode(2, subject);
     } else {
-      scr_set_multimode(1, subj_utf8);
-      verbat = FALSE;
+      scr_set_multimode(1, subject);
     }
 
     scr_LogPrint(LPRINT_NORMAL, "Entered %smulti-line message mode.",
-                 verbat ? "VERBATIM " : "");
+                 subcmd == scmd_msay_verbatim ? "VERBATIM " : "");
     scr_LogPrint(LPRINT_NORMAL, "Select a buddy and use \"%s send\" "
                  "when your message is ready.", mkcmdstr("msay"));
-    if (verbat)
+    if (subcmd == scmd_msay_verbatim)
       scr_LogPrint(LPRINT_NORMAL, "Use \"%s abort\" to abort this mode.",
                    mkcmdstr("msay"));
-    g_free(subj_utf8);
-    goto do_msay_return;
-  } else if (strcasecmp(subcmd, "send") && strcasecmp(subcmd, "send_to")) {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-    goto do_msay_return;
+    return NULL;
   }
 
-  /* send/send_to command */
+  /* scmd_msay_send or scmd_msay_send_to */
 
   if (!scr_get_multimode()) {
-    scr_LogPrint(LPRINT_NORMAL, "No message to send.  "
+    return g_strdup_printf ("No message to send.  "
                  "Use \"%s begin\" first.", mkcmdstr("msay"));
-    goto do_msay_return;
   }
 
   scr_set_chatmode(TRUE);
   scr_show_buddy_window();
 
-  if (!strcasecmp(subcmd, "send_to")) {
-    int err = FALSE;
-    gchar *msg_utf8;
-    LmMessageSubType msg_type = scan_mtype(&arg);
-    // Let's send to the specified JID.  We leave now if there
-    // has been an error (so we don't leave multi-line mode).
-    arg = to_utf8(arg);
-    msg_utf8 = to_utf8(scr_get_multiline());
-    if (msg_utf8) {
-      err = send_message_to(arg, msg_utf8, scr_get_multimode_subj(), msg_type,
-                            FALSE);
-      g_free(msg_utf8);
-    }
-    g_free(arg);
-    if (err)
-      goto do_msay_return;
-  } else { // Send to currently selected buddy
-    gpointer bud;
-    gchar *msg_utf8;
-
-    if (!current_buddy) {
-      scr_LogPrint(LPRINT_NORMAL, "Whom are you talking to?");
-      goto do_msay_return;
-    }
-
-    bud = BUDDATA(current_buddy);
-    if (!(buddy_gettype(bud) &
-          (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM))) {
-      scr_LogPrint(LPRINT_NORMAL, "This is not a user.");
-      goto do_msay_return;
-    }
-
-    buddy_setflags(bud, ROSTER_FLAG_LOCK, TRUE);
-    msg_utf8 = to_utf8(scr_get_multiline());
-    if (msg_utf8) {
-      send_message(msg_utf8, scr_get_multimode_subj(), scan_mtype(&arg));
-      g_free(msg_utf8);
+  if ((msg = scr_get_multiline())) {
+    msgtype_t msg_type = (msgtype_t) (values[pos_msay_msgtype].src -> userdata);
+
+    if (subcmd == scmd_msay_send_to) {
+      const char *jid = values[pos_msay_jid].value.arg;
+
+      // Let's send to the specified JID.  We leave now if there
+      // has been an error (so we don't leave multi-line mode).
+      if (send_message_to(jid, msg, scr_get_multimode_subj(),
+                          msg_type, FALSE))
+        return NULL;
+    } else { // Send to currently selected buddy
+      gpointer bud;
+
+      if (!current_buddy) {
+        return g_strdup ("Whom are you talking to?");
+      }
+
+      bud = BUDDATA(current_buddy);
+      if (!(buddy_gettype(bud) &
+            (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM))) {
+        return g_strdup ("This is not a user.");
+      }
+
+      buddy_setflags(bud, ROSTER_FLAG_LOCK, TRUE);
+      send_message(msg, scr_get_multimode_subj(), msg_type);
     }
   }
+
   scr_set_multimode(FALSE, NULL);
   scr_LogPrint(LPRINT_NORMAL, "You have left multi-line message mode.");
-do_msay_return:
-  free_arg_lst(paramlst);
+
+  return NULL;
 }
 
+//
+//  /say_to
+//
+
 //  load_message_from_file(filename)
 // Read the whole content of a file.
 // The data are converted to UTF8, they should be freed by the caller after
 // use.
-char *load_message_from_file(const char *filename)
+char *load_message_from_file(const char *filename, const char *localfn)
 {
   FILE *fd;
   struct stat buf;
@@ -1566,7 +2463,7 @@
   char *next_utf8_char;
   size_t len;
 
-  fd = fopen(filename, "r");
+  fd = fopen(localfn, "r");
 
   if (!fd || fstat(fileno(fd), &buf)) {
     scr_LogPrint(LPRINT_LOGNORM, "Cannot open message file (%s)", filename);
@@ -1632,273 +2529,287 @@
   return msgbuf_utf8;
 }
 
-static void do_say_to(char *arg)
+static gchar *do_say_to (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_sayto_jid     = 0,
+  pos_sayto_msg     = 1,
+  pos_sayto_msgtype = 2,
+  pos_sayto_quiet   = 3,
+  pos_sayto_esc     = 4,
+  pos_sayto_file    = 5,
+} pos_sayto_t;
+
+static cmdopts_t def_say_to = {
+  "say_to",
+  cmd_default,
+  cmd_check_online,
+  do_say_to,
+  (cmdopt_t[7]){
+    {'n', "normal",   {"normal",   pos_sayto_msgtype, cmdarg_switch,   NULL, NULL, NULL, (gpointer)msgtype_normal}},
+    {'h', "headline", {"headline", pos_sayto_msgtype, cmdarg_switch,   NULL, NULL, NULL, (gpointer)msgtype_headline}},
+    {'d', "default",  {"default",  pos_sayto_msgtype, cmdarg_switch,   NULL, NULL, NULL, (gpointer)msgtype_not_set}},
+    {'q', "quiet",    {"quiet",    pos_sayto_quiet,   cmdarg_trigger,  NULL, NULL}},
+    {'e', "escapes",  {"escapes",  pos_sayto_esc,     cmdarg_trigger,  NULL, NULL}},
+    {'f', "file",     {"filename", pos_sayto_file,    cmdarg_required, NULL, &cmdarg_type_filename}},
+    {0}
+  },
+  (cmdarg_t[3]){
+    {"jid",     pos_sayto_jid, cmdarg_chreq, ".",  &cmdarg_type_fjid, (gpointer)cmdarg_roster_entity},
+    {"message", pos_sayto_msg, cmdarg_eol,   NULL, &cmdarg_type_nonspace},
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_say_to (const cmdopts_t *command, cmdarg_value_t *values)
 {
-  char **paramlst;
-  char *fjid, *msg_utf8;
-  char *msg;
-  char *unescaped_msg = NULL;
-  char *uncompletedfjid = NULL;
-  char *file = NULL;
-  LmMessageSubType msg_type = LM_MESSAGE_SUB_TYPE_NOT_SET;
-  bool quiet = FALSE;
-  bool eval = FALSE;
-
-  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;
-  }
-
-  // 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 (!*paramlst) {
-    scr_LogPrint(LPRINT_NORMAL, "Wrong usage.");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  fjid = *paramlst;
-  msg = *(paramlst+1);
-
-  if (fjid[0] == '.') {
-    const gchar *cjid = (current_buddy ? CURRENT_JID : NULL);
-    if (fjid[1] == '\0') {
-      fjid = g_strdup(cjid);
-    } 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);
-      g_free(res_utf8);
-    } else
-      fjid = to_utf8(fjid);
-  } else
-    fjid = to_utf8(fjid);
-
+  char *fjid, *msg, *file;
+  gchar *freeme  = NULL; // fjid
+  gchar *freeme2 = NULL; // msg
+  msgtype_t msg_type = (msgtype_t) (values[pos_sayto_msgtype].src -> userdata);
+
+  fjid = values[pos_sayto_jid].value.arg;
+  msg  = values[pos_sayto_msg].value.arg;
+  file = values[pos_sayto_file].value.fname.utf8;
+
+  // XXX this is not yet in default fjid checker. should we add it there?
   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;
     }
   }
 
-  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);
-    return;
-  }
-
-  if (!file) {
-    msg_utf8 = to_utf8(msg);
-    if (eval) {
-      unescaped_msg = ut_unescape_tabs_cr(msg_utf8);
+  if (file == NULL) {
+    if (values[pos_sayto_esc].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);
-    g_free(filename_xp);
-    g_free(file);
+    freeme2 = msg = load_message_from_file(file,
+                                           values[pos_sayto_file].value.fname.local);
   }
 
-  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, values[pos_sayto_quiet].value.swc);
+
+  g_free(freeme);
+  g_free(freeme2);
+
+  return NULL;
 }
 
-//  buffer_updown(updown, nblines)
-// updown: -1=up, +1=down
-inline static void buffer_updown(int updown, char *nlines)
+//
+//  /buffer
+//
+
+static gchar *do_buffer (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  scmd_buffer_close, scmd_buffer_close_all,
+  scmd_buffer_clear, scmd_buffer_purge,
+  scmd_buffer_list,
+  scmd_buffer_top, scmd_buffer_bottom, scmd_buffer_up, scmd_buffer_down,
+  scmd_buffer_date, scmd_buffer_percent, scmd_buffer_readmark,
+  scmd_buffer_search_backward, scmd_buffer_search_forward,
+  scmd_buffer_scroll_lock, scmd_buffer_scroll_unlock,
+  scmd_buffer_scroll_toggle,
+  scmd_buffer_save,
+} scmd_buffer_t;
+
+typedef enum {
+  pos_buffer_scmd      = 0,
+  pos_buffer_jid       = 1,
+  pos_buffer_n         = 1,
+  pos_buffer_date      = 1,
+  pos_buffer_percent   = 1,
+  pos_buffer_text      = 1,
+  pos_buffer_file      = 1,
+} pos_buffer_t;
+
+static cmdopts_t def_buffer = {
+  "buffer",
+  cmd_default,
+  cmd_check_current_buddy,
+  do_buffer,
+  NULL,
+  (cmdarg_t[2]){
+    { "subcommand", pos_buffer_scmd, cmdarg_subcmd|cmdarg_chreq, NULL, NULL },
+    {NULL},
+  },
+  (cmdopts_t[19]){
+    { "close", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "jid", pos_buffer_jid, cmdarg_required, NULL, &cmdarg_type_roster_bjid, (gpointer)cmdarg_roster_buffer },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_close },
+    { "close_all", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_close_all },
+    { "clear", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_clear },
+    { "purge", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "jid", pos_buffer_jid, cmdarg_required, NULL, &cmdarg_type_roster_bjid, (gpointer)cmdarg_roster_buffer },
+        {0}
+      },
+      NULL, (gpointer)scmd_buffer_purge },
+    { "list", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_list },
+    { "top",    cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_top    },
+    { "bottom", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_bottom },
+    { "up", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "n", pos_buffer_n, cmdarg_chreq, "0", &cmdarg_type_uint, (gpointer)0 },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_up },
+    { "down", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "n", pos_buffer_n, cmdarg_chreq, "0", &cmdarg_type_uint, (gpointer)0 },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_down },
+    { "date", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "date", pos_buffer_date, cmdarg_chreq, "1", &cmdarg_type_date },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_date },
+    { "%", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "percent", pos_buffer_percent, cmdarg_chreq, "100", &cmdarg_type_uint, (gpointer)100 },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_percent },
+    { "readmark", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_readmark },
+    { "search_backward", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "text", pos_buffer_text, cmdarg_chreq, NULL, &cmdarg_type_nonspace },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_search_backward },
+    { "search_forward", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "text", pos_buffer_text, cmdarg_chreq, NULL, &cmdarg_type_nonspace },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_search_forward },
+    { "scroll_lock",   cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_scroll_lock   },
+    { "scroll_unlock", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_scroll_unlock },
+    { "scroll_toggle", cmd_default, NULL, NULL, NULL, NULL, NULL, (gpointer)scmd_buffer_scroll_toggle },
+    { "save", cmd_default, NULL, NULL, NULL,
+      (cmdarg_t[2]){
+        { "filename", pos_buffer_file, cmdarg_chreq, NULL, &cmdarg_type_filename },
+        {NULL}
+      },
+      NULL, (gpointer)scmd_buffer_save },
+  },
+};
+
+// XXX % command before was able to handle %50
+static gchar *do_buffer (const cmdopts_t *command, cmdarg_value_t *values)
 {
-  int nblines;
-
-  if (!nlines || !*nlines)
-    nblines = 0;
-  else
-    nblines = strtol(nlines, NULL, 10);
-
-  if (nblines >= 0)
-    scr_buffer_scroll_up_down(updown, nblines);
+  scmd_buffer_t subcmd = (scmd_buffer_t) (values[pos_buffer_scmd].value.cmd -> userdata);
+
+  if (buddy_gettype(BUDDATA(current_buddy)) & ROSTER_TYPE_GROUP &&
+      subcmd != scmd_buffer_close_all) {
+    return g_strdup ("Groups have no buffer.");
+  }
+
+  if (subcmd == scmd_buffer_close) {
+    scr_buffer_purge(1, buddy_getjid(values[pos_buffer_jid].value.rjid.bud));
+  } else if (subcmd == scmd_buffer_close_all) {
+    scr_buffer_purge_all(1);
+  } else if (subcmd == scmd_buffer_clear) {
+    scr_buffer_clear();
+  } else if (subcmd == scmd_buffer_purge) {
+    scr_buffer_purge(0, buddy_getjid(values[pos_buffer_jid].value.rjid.bud));
+  } else if (subcmd == scmd_buffer_list) {
+    scr_buffer_list();
+  } else if (subcmd == scmd_buffer_top) {
+    scr_buffer_top_bottom(-1);
+  } else if (subcmd == scmd_buffer_bottom) {
+    scr_buffer_top_bottom(1);
+  } else if (subcmd == scmd_buffer_up) {
+    scr_buffer_scroll_up_down(-1, values[pos_buffer_n].value.uint);
+  } else if (subcmd == scmd_buffer_down) {
+    scr_buffer_scroll_up_down(1, values[pos_buffer_n].value.uint);
+  } else if (subcmd == scmd_buffer_date) {
+    scr_buffer_date(values[pos_buffer_date].value.time);
+  } else if (subcmd == scmd_buffer_percent) {
+    scr_buffer_percent(values[pos_buffer_percent].value.uint);
+  } else if (subcmd == scmd_buffer_readmark) {
+    scr_buffer_jump_readmark();
+  } else if (subcmd == scmd_buffer_search_backward) {
+    scr_buffer_search(-1, values[pos_buffer_text].value.arg);
+  } else if (subcmd == scmd_buffer_search_forward) {
+    scr_buffer_search(1, values[pos_buffer_text].value.arg);
+  } else if (subcmd == scmd_buffer_scroll_lock) {
+    scr_buffer_scroll_lock(1);
+  } else if (subcmd == scmd_buffer_scroll_unlock) {
+    scr_buffer_scroll_lock(0);
+  } else if (subcmd == scmd_buffer_scroll_toggle) {
+    scr_buffer_scroll_lock(-1);
+  } else { // scmd_buffer_save
+    scr_buffer_dump(values[pos_buffer_file].value.fname.local);
+  }
+
+  return NULL;
 }
 
-static void buffer_search(int direction, char *arg)
+//
+//  /clear
+//
+
+// XXX convert to real alias?
+static gchar *do_clear (const cmdopts_t *command, cmdarg_value_t *values);
+
+static cmdopts_t def_clear = {
+  "clear",
+  cmd_default,
+  cmd_check_current_buddy,
+  do_clear,
+  NULL,
+  NULL,
+  NULL,
+};
+
+// Alias for "buffer clear"
+static gchar *do_clear(const cmdopts_t *command, cmdarg_value_t *values)
 {
-  if (!arg || !*arg) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    return;
-  }
-
-  scr_buffer_search(direction, arg);
+  scr_buffer_clear();
+  return NULL;
 }
 
-static void buffer_date(char *date)
-{
-  time_t t;
-
-  if (!date || !*date) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    return;
-  }
-
-  strip_arg_special_chars(date);
-
-  t = from_iso8601(date, 0);
-  if (t)
-    scr_buffer_date(t);
-  else
-    scr_LogPrint(LPRINT_NORMAL, "The date you specified is "
-                 "not correctly formatted or invalid.");
-}
-
-static void buffer_percent(char *arg1, char *arg2)
-{
-  // Basically, user has typed "%arg1 arg2"
-  // "%50"  -> arg1 = 50, arg2 null pointer
-  // "% 50" -> arg1 = \0, arg2 = 50
-
-  if (!*arg1 && (!arg2 || !*arg2)) { // No value
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    return;
-  }
-
-  if (*arg1 && arg2 && *arg2) {     // Two values
-    scr_LogPrint(LPRINT_NORMAL, "Wrong parameters.");
-    return;
-  }
-
-  scr_buffer_percent(atoi((*arg1 ? arg1 : arg2)));
-}
-
-static void do_buffer(char *arg)
-{
-  char **paramlst;
-  char *subcmd;
-
-  if (!current_buddy)
-    return;
-
-  paramlst = split_arg(arg, 2, 1); // subcmd, arg
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  if (buddy_gettype(BUDDATA(current_buddy)) & ROSTER_TYPE_GROUP &&
-      strcasecmp(subcmd, "close_all")) {
-    scr_LogPrint(LPRINT_NORMAL, "Groups have no buffer.");
-    free_arg_lst(paramlst);
-    return;
-  }
-
-  if (!strcasecmp(subcmd, "top")) {
-    scr_buffer_top_bottom(-1);
-  } else if (!strcasecmp(subcmd, "bottom")) {
-    scr_buffer_top_bottom(1);
-  } else if (!strcasecmp(subcmd, "clear")) {
-    scr_buffer_clear();
-  } else if (!strcasecmp(subcmd, "close")) {
-    scr_buffer_purge(1, arg);
-  } else if (!strcasecmp(subcmd, "close_all")) {
-    scr_buffer_purge_all(1);
-  } else if (!strcasecmp(subcmd, "purge")) {
-    scr_buffer_purge(0, arg);
-  } else if (!strcasecmp(subcmd, "scroll_lock")) {
-    scr_buffer_scroll_lock(1);
-  } else if (!strcasecmp(subcmd, "scroll_unlock")) {
-    scr_buffer_scroll_lock(0);
-  } else if (!strcasecmp(subcmd, "scroll_toggle")) {
-    scr_buffer_scroll_lock(-1);
-  } else if (!strcasecmp(subcmd, "up")) {
-    buffer_updown(-1, arg);
-  } else if (!strcasecmp(subcmd, "down")) {
-    buffer_updown(1, arg);
-  } else if (!strcasecmp(subcmd, "search_backward")) {
-    strip_arg_special_chars(arg);
-    buffer_search(-1, arg);
-  } else if (!strcasecmp(subcmd, "search_forward")) {
-    strip_arg_special_chars(arg);
-    buffer_search(1, arg);
-  } else if (!strcasecmp(subcmd, "date")) {
-    buffer_date(arg);
-  } else if (*subcmd == '%') {
-    buffer_percent(subcmd+1, arg);
-  } else if (!strcasecmp(subcmd, "save")) {
-    scr_buffer_dump(arg);
-  } else if (!strcasecmp(subcmd, "list")) {
-    scr_buffer_list();
-  } else if (!strcasecmp(subcmd, "readmark")) {
-    scr_buffer_jump_readmark();
-  } else {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-  }
-
-  free_arg_lst(paramlst);
-}
-
-static void do_clear(char *arg)    // Alias for "buffer clear"
-{
-  do_buffer("clear");
-}
-
-static void do_info(char *arg)
+//
+//  /info
+//
+
+static gchar *do_info (const cmdopts_t *command, cmdarg_value_t *values);
+
+static cmdopts_t def_info = {
+  "clear",
+  cmd_default,
+  cmd_check_current_buddy,
+  do_info,
+  NULL,
+  NULL,
+  NULL,
+};
+
+static gchar *do_info (const cmdopts_t *command, cmdarg_value_t *values)
 {
   gpointer bud;
   const char *bjid, *name;
@@ -1906,9 +2817,7 @@
   char *buffer;
   enum subscr esub;
 
-  if (!current_buddy)
-    return;
-  bud = BUDDATA(current_buddy);
+  bud    = BUDDATA(current_buddy);
 
   bjid   = buddy_getjid(bud);
   name   = buddy_getname(bud);
@@ -2031,95 +2940,13 @@
                                HBB_PREFIX_INFO, 0);
     }
   }
+
+  return NULL;
 }
 
-// room_names() is a variation of do_info(), for chatrooms only
-static void room_names(gpointer bud, char *arg)
-{
-  const char *bjid;
-  char *buffer;
-  GSList *resources, *p_res;
-  enum { style_normal = 0, style_detail, style_short,
-         style_quiet, style_compact } style = 0;
-
-  if (*arg) {
-    if (!strcasecmp(arg, "--short"))
-      style = style_short;
-    else if (!strcasecmp(arg, "--quiet"))
-      style = style_quiet;
-    else if (!strcasecmp(arg, "--detail"))
-      style = style_detail;
-    else if (!strcasecmp(arg, "--compact"))
-      style = style_compact;
-    else {
-      scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-      return;
-    }
-  }
-
-  // Enter chat mode
-  scr_set_chatmode(TRUE);
-  scr_show_buddy_window();
-
-  bjid = buddy_getjid(bud);
-
-  buffer = g_slice_alloc(4096);
-  strncpy(buffer, "Room members:", 127);
-  scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
-
-  resources = buddy_getresources(bud);
-  for (p_res = resources ; p_res ; p_res = g_slist_next(p_res)) {
-    enum imstatus rstatus;
-    const char *rst_msg;
-
-    rstatus = buddy_getstatus(bud, p_res->data);
-    rst_msg = buddy_getstatusmsg(bud, p_res->data);
-
-    if (style == style_short) {
-      snprintf(buffer, 4095, "[%c] %s%s%s", imstatus2char[rstatus],
-               (char*)p_res->data,
-               rst_msg ? " -- " : "", rst_msg ? rst_msg : "");
-      scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
-    } else if (style == style_compact) {
-        enum imrole role = buddy_getrole(bud, p_res->data);
-        enum imaffiliation affil = buddy_getaffil(bud, p_res->data);
-        bool showaffil = (affil != affil_none);
-
-        snprintf(buffer, 4095, "[%c] %s (%s%s%s)",
-                 imstatus2char[rstatus], (char*)p_res->data,
-                 showaffil ? straffil[affil] : "\0",
-                 showaffil ? "/" : "\0",
-                 strrole[role]);
-        scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
-      } else {
-      // (Style "normal", "detail" or "quiet")
-      snprintf(buffer, 4095, "[%c] %s", imstatus2char[rstatus],
-               (char*)p_res->data);
-      scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
-      if (rst_msg && style != style_quiet) {
-        snprintf(buffer, 4095, "Status message: %s", rst_msg);
-        scr_WriteIncomingMessage(bjid, buffer,
-                                 0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
-      }
-      if (style == style_detail) {
-        enum imrole role = buddy_getrole(bud, p_res->data);
-        enum imaffiliation affil = buddy_getaffil(bud, p_res->data);
-
-        snprintf(buffer, 4095, "Role: %s", strrole[role]);
-        scr_WriteIncomingMessage(bjid, buffer,
-                                 0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
-        if (affil != affil_none) {
-          snprintf(buffer, 4095, "Affiliat.: %s", straffil[affil]);
-          scr_WriteIncomingMessage(bjid, buffer,
-                                   0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
-        }
-      }
-    }
-    g_free(p_res->data);
-  }
-  g_slist_free(resources);
-  g_slice_free1(4096, buffer);
-}
+//
+//  /rename
+//
 
 static void move_group_member(gpointer bud, void *groupnamedata)
 {
@@ -2128,13 +2955,13 @@
 
   groupname = (char *)groupnamedata;
 
-  bjid = buddy_getjid(bud);
-  name = buddy_getname(bud);
-  type = buddy_gettype(bud);
+  bjid   = buddy_getjid(bud);
+  name   = buddy_getname(bud);
+  type   = buddy_gettype(bud);
   on_srv = buddy_getonserverflag(bud);
 
   if (on_srv)
-    xmpp_updatebuddy(bjid, name, *groupname ? groupname : NULL);
+    xmpp_updatebuddy(bjid, name, groupname);
   else {
     buddy_setgroup(bud, (char *)groupname);
     if ((type & ROSTER_TYPE_ROOM) && xmpp_is_bookmarked(bjid) &&
@@ -2143,32 +2970,52 @@
   }
 }
 
-static void do_rename(char *arg)
+// command
+
+static gchar *do_rename (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_rename_jid   = 0,
+  pos_rename_name  = 1,
+  pos_rename_reset = 2,
+} pos_rename_t;
+
+// XXX:
+//  * custom type for completion by existing roster entry names
+static cmdopts_t def_rename = {
+  "rename",
+  cmd_default,
+  NULL,
+  do_rename,
+  (cmdopt_t[5]){
+    { 'r', "reset", { "reset", pos_rename_reset, cmdarg_switch, NULL, NULL } },
+    { 'n', "name",  { "name",  pos_rename_jid, cmdarg_chreq, ".", &cmdarg_type_roster_name, (gpointer)cmdarg_roster_normal|cmdarg_roster_name } },
+    { 'g', "group", { "group", pos_rename_jid, cmdarg_chreq, ".", &cmdarg_type_roster_group } },
+    { 'j', "jid",   { "jid",   pos_rename_jid, cmdarg_chreq, ".", &cmdarg_type_roster_bjid, (gpointer)cmdarg_roster_entity } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "new name", pos_rename_name, cmdarg_chreq, NULL, &cmdarg_type_nonspace },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_rename (const cmdopts_t *command, cmdarg_value_t *values)
 {
   gpointer bud;
-  const char *bjid, *group;
+  const char *bjid;
   guint type, on_srv;
-  char *newname, *p;
-  char *name_utf8;
-
-  if (!current_buddy)
-    return;
-  bud = BUDDATA(current_buddy);
+  char *newname = NULL;
+  gboolean reset;
+
+  bud     = values[pos_rename_jid].value.rjid.bud;
+  reset   = values[pos_rename_reset].value.swc;
+  newname = values[pos_rename_name].value.arg;
 
   bjid   = buddy_getjid(bud);
-  group  = buddy_getgroupname(bud);
+  on_srv = buddy_getonserverflag(bud);
   type   = buddy_gettype(bud);
-  on_srv = buddy_getonserverflag(bud);
-
-  if (type & ROSTER_TYPE_SPECIAL) {
-    scr_LogPrint(LPRINT_NORMAL, "You can't rename this item.");
-    return;
-  }
-
-  if (!*arg && !(type & ROSTER_TYPE_GROUP)) {
-    scr_LogPrint(LPRINT_NORMAL, "Please specify a new name.");
-    return;
-  }
 
   //if (!(type & ROSTER_TYPE_GROUP) && !on_srv) {
   //  scr_LogPrint(LPRINT_NORMAL,
@@ -2181,90 +3028,88 @@
   //  }
   //}
 
-  newname = g_strdup(arg);
-  // Remove trailing space
-  for (p = newname; *p; p++) ;
-  while (p > newname && *p == ' ') *p = 0;
-
-  strip_arg_special_chars(newname);
-
-  name_utf8 = to_utf8(newname);
-
   if (type & ROSTER_TYPE_GROUP) {
     // Rename a whole group
-    foreach_group_member(bud, &move_group_member, name_utf8);
+    foreach_group_member(bud, &move_group_member, (reset ? NULL : newname));
     // Let's jump to the previous buddy, because this group name should
     // disappear when we receive the server answer.
-    scr_roster_up_down(-1, 1);
+    // XXX: we can now "disappear" arbitrary buddy/group.
+    // Probably, we need some hook, when something appears/disappears,
+    // so that ui can know and react to that when it really happens, rather
+    // than doing this here.
+    //scr_roster_up_down(-1, 1);
   } else {
     // Rename a single buddy
-    guint del_name = 0;
-    if (!*newname || !strcmp(arg, "-"))
-      del_name = TRUE;
     if (on_srv) {
-      /* We do not rename the buddy right now because the server could reject
-       * the request.  Let's wait for the server answer.
-       */
-      xmpp_updatebuddy(bjid, (del_name ? NULL : name_utf8),
+      const char *group = buddy_getgroupname(bud);
+      // We do not rename the buddy right now because the server could reject
+      // the request.  Let's wait for the server answer.
+      xmpp_updatebuddy(bjid, (reset ? NULL : newname),
                        group && *group ? group : NULL);
     } else {
       // This is a local item, we rename it without adding to roster.
-      buddy_setname(bud, (del_name ? (char*)bjid : name_utf8));
+      buddy_setname(bud, (reset ? (char*)bjid : newname));
       if ((type & ROSTER_TYPE_ROOM) && xmpp_is_bookmarked(bjid) &&
           settings_opt_get_int("muc_bookmark_autoupdate"))
         room_bookmark(bud, NULL);
     }
   }
 
-  g_free(name_utf8);
-  g_free(newname);
   update_roster = TRUE;
+
+  return NULL;
 }
 
-static void do_move(char *arg)
+static gchar *do_move (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_move_jid   = 0,
+  pos_move_name  = 1,
+} pos_move_t;
+
+// XXX:
+//  * custom type for completion by existing roster group names
+//    (share with rename, using types in chkdata?)
+static cmdopts_t def_move = {
+  "move",
+  cmd_default,
+  NULL,
+  do_move,
+  (cmdopt_t[5]){
+    { 'n', "name",  { "name",  pos_move_jid, cmdarg_chreq, ".", &cmdarg_type_roster_name, (gpointer)cmdarg_roster_entity|cmdarg_roster_name } },
+    { 'j', "jid",   { "jid",   pos_move_jid, cmdarg_chreq, ".", &cmdarg_type_roster_bjid, (gpointer)cmdarg_roster_entity } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "new name", pos_move_name, cmdarg_eol|cmdarg_required, NULL, &cmdarg_type_nonspace },
+    {NULL}
+  },
+  NULL,
+};
+
+static gchar *do_move (const cmdopts_t *command, cmdarg_value_t *values)
 {
   gpointer bud;
   const char *bjid, *name, *oldgroupname;
   guint type, on_srv;
-  char *newgroupname, *p;
-  char *group_utf8;
-
-  if (!current_buddy)
-    return;
-  bud = BUDDATA(current_buddy);
-
-  bjid = buddy_getjid(bud);
-  name = buddy_getname(bud);
-  type = buddy_gettype(bud);
-  on_srv = buddy_getonserverflag(bud);
-
+  char *newgroupname;
+
+  bud          = values[pos_move_jid].value.rjid.bud;
+  newgroupname = values[pos_move_name].value.arg;
+
+  bjid         = buddy_getjid(bud);
+  name         = buddy_getname(bud);
+  type         = buddy_gettype(bud);
+  on_srv       = buddy_getonserverflag(bud);
   oldgroupname = buddy_getgroupname(bud);
 
-  if (type & ROSTER_TYPE_GROUP) {
-    scr_LogPrint(LPRINT_NORMAL, "You can't move groups!");
-    return;
-  }
-  if (type & ROSTER_TYPE_SPECIAL) {
-    scr_LogPrint(LPRINT_NORMAL, "You can't move this item.");
-    return;
-  }
-
-  newgroupname = g_strdup(arg);
-  // Remove trailing space
-  for (p = newgroupname; *p; p++) ;
-  while (p > newgroupname && *p == ' ') *p-- = 0;
-
-  strip_arg_special_chars(newgroupname);
-
-  group_utf8 = to_utf8(newgroupname);
-  if (strcmp(oldgroupname, group_utf8)) {
+  if (strcmp(oldgroupname, newgroupname)) {
     if (on_srv) {
-      xmpp_updatebuddy(bjid, name, *group_utf8 ? group_utf8 : NULL);
-      scr_roster_up_down(-1, 1);
-
-      /* We do not move the buddy right now because the server could reject
-       * the request.  Let's wait for the server answer.
-       */
+      xmpp_updatebuddy(bjid, name, newgroupname);
+      // XXX see /roster
+      //scr_roster_up_down(-1, 1);
+      // We do not move the buddy right now because the server could reject
+      // the request.  Let's wait for the server answer.
     } else {
       // This is a local item, we move it without adding to roster.
       guint msgflag;
@@ -2276,7 +3121,7 @@
       msgflag = buddy_getflags(bud) & ROSTER_FLAG_MSG;
       if (msgflag)
         roster_msg_setflag(bjid, FALSE, FALSE);
-      buddy_setgroup(bud, group_utf8);
+      buddy_setgroup(bud, newgroupname);
       if (msgflag)
         roster_msg_setflag(bjid, FALSE, TRUE);
       if ((type & ROSTER_TYPE_ROOM) && xmpp_is_bookmarked(bjid) &&
@@ -2285,33 +3130,160 @@
     }
   }
 
-  g_free(group_utf8);
-  g_free(newgroupname);
   update_roster = TRUE;
+
+  return NULL;
 }
 
-static void list_option_cb(char *k, char *v, void *f)
+//
+//  /set
+//
+
+static gchar *do_set (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  pos_set_assignment = 0,
+  pos_set_reset      = 1,
+  pos_set_file       = 2,
+} pos_set_t;
+
+static cmdopts_t *def_set = {
+  "set",
+  cmd_safe,
+  NULL,
+  do_setting,
+  (cmdopt_t[3]){
+    { 'd', "dump",  { "filename", pos_set_file,  cmdarg_required, NULL, &cmdarg_type_filename } },
+    { 'r', "reset", { "reset",    pos_set_reset, cmdarg_trigger,  NULL, NULL } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "assignment", pos_set_assignment, cmdarg_eol|cmdarg_required, NULL, &cmdarg_type_assignment },
+    {NULL}
+  },
+  NULL,
+  (gpointer)SETTINGS_TYPE_OPTION
+};
+
+static cmdopts_t *def_alias = {
+  "alias",
+  cmd_safe,
+  NULL,
+  do_setting,
+  (cmdopt_t[3]){
+    { 'd', "dump",  { "filename", pos_set_file,  cmdarg_required, NULL, &cmdarg_type_filename } },
+    { 'r', "reset", { "reset",    pos_set_reset, cmdarg_trigger,  NULL, NULL } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "assignment", pos_set_assignment, cmdarg_eol|cmdarg_required, NULL, &cmdarg_type_assignment },
+    {NULL}
+  },
+  NULL,
+  (gpointer)SETTINGS_TYPE_ALIAS
+};
+
+static cmdopts_t *def_bind = {
+  "bind",
+  cmd_safe,
+  NULL,
+  do_setting,
+  (cmdopt_t[3]){
+    { 'd', "dump",  { "filename", pos_set_file,  cmdarg_required, NULL, &cmdarg_type_filename } },
+    { 'r', "reset", { "reset",    pos_set_reset, cmdarg_trigger,  NULL, NULL } },
+    {0}
+  },
+  (cmdarg_t[2]){
+    { "assignment", pos_set_assignment, cmdarg_eol|cmdarg_required, NULL, &cmdarg_type_assignment },
+    {NULL}
+  },
+  NULL,
+  (gpointer)SETTINGS_TYPE_BINDING
+};
+
+static const setting_cb_t def_set_data = {
+  SETTINGS_TYPE_OPTION,
+  "%%-%us = [%%s]",
+  "No options found",
+  "Option %s is not set",
+  "%s = [%s]",
+  "set %%-%us = %%s",
+};
+
+static void settings_build_namelist_cb (char *k, char *v, void *f)
 {
   GSList **list = f;
   *list = g_slist_insert_sorted(*list, k, (GCompareFunc)strcmp);
 }
 
-static void do_set(char *arg)
+static void setting_dump_to_file_cb (char *key, char *value, void *userdata)
 {
-  guint assign;
-  gchar *option, *value;
-  gchar *option_utf8;
-
-  if (!*arg) {
-    // list all set options
-    GSList *list = NULL;
-    // Get sorted list of keys
-    settings_foreach(SETTINGS_TYPE_OPTION, list_option_cb, &list);
+  // foo cb = ...;
+  GString *line = cb....;
+  // foo file = cb....;
+  g_string_printf (line, "%s %s = \"", command -> name, key);
+  // unsecape value
+  key = value;
+  do {
+    if (*key == '"' || *key == '\\') {
+      g_string_append_len (line, value, key - value);
+      g_string_append_c (line, '\\');
+      value = key;
+      key ++;
+    } else if (*key == '\0') {
+      g_string_append_len (line, value, key - value);
+    } else {
+      key ++;
+    }
+  } while (*key);
+  g_string_append_c (line, '"');
+  // write the line here
+}
+
+// eol      = as is
+// plain    = only one word
+// catchall = backescape quotes/escapes
+// default  = quote
+static void cmdarg_unescape (
+
+static gchar *do_setting (const cmdopts_t *command, cmdarg_value_t *values)
+{
+  gchar    *option    = values[pos_set_assignment].value.assign.key;
+  gchar    *value     = values[pos_set_assignment].value.assign.value;
+  gboolean assignment = values[pos_set_assignment].value.assign.assignment;
+  guint    stype     = (guint)(command -> userdata);
+  gboolean reset = XXX;
+  const char *template = "%%-%us = [%%s]";
+  const char *msg1 = "No options found.";
+  const char *msg2 = "Option %s is not set";
+  const char *msg3 = "%s = [%s]";
+  gchar *filename = values[pos_set_filename].value.fname.local;
+
+    // open file here
+    GString *line = g_string_new (NULL);
+    // foo cb = {..., line};
+    if (!option) {
+      settings_foreach(stype, settings_dump_to_file_cb, cb);
+    } else {
+      settings_dump_to_file_cb (option, settings_get (stype, option), cb);
+    }
+    g_string_free (line, TRUE);
+    // close file here
+  }
+
+  if (option == NULL) {
+    GSList *list  = NULL;
+    
+    settings_foreach(stype, settings_build_namelist_cb, &list);
+
     if (list) {
-      gsize max = 0;
-      gsize maxmax = scr_gettextwidth() / 3;
+      gsize  max    = 0;
+      gsize  maxmax = scr_gettextwidth() / 3;
       GSList *lel;
-      gchar *format;
+      gchar  *format;
+
+      // Get sorted list of keys
+
       // Find out maximum key length
       for (lel = list; lel; lel = lel->next) {
         const gchar *key = lel->data;
@@ -2324,50 +3296,39 @@
           }
         }
       }
+
       // Print out list of options
-      format = g_strdup_printf("%%-%us = [%%s]", (unsigned)max);
+      format = g_strdup_printf(template, (unsigned)max);
       for (lel = list; lel; lel = lel->next) {
         const gchar *key = lel->data;
-        scr_LogPrint(LPRINT_NORMAL, format, key, settings_opt_get(key));
+        scr_log_print(LPRINT_NORMAL, format, key, settings_get(stype, key));
       }
       g_free(format);
       scr_setmsgflag_if_needed(SPECIAL_BUFFER_STATUS_ID, TRUE);
       scr_setattentionflag_if_needed(SPECIAL_BUFFER_STATUS_ID, TRUE,
                                  ROSTER_UI_PRIO_STATUS_WIN_MESSAGE, prio_max);
-    } else
-      scr_LogPrint(LPRINT_NORMAL, "No options found.");
-    return;
+    } else {
+      scr_log_print (LPRINT_NORMAL, "No options found.");
+    }
+  } else if (value == NULL && !reset) {
+    const char *val = settings_opt_get(option);
+    if (val)
+      scr_LogPrint(LPRINT_NORMAL, "%s = [%s]", option, val);
+    else
+      scr_LogPrint(LPRINT_NORMAL, "Option %s is not set", option);
+  } else {
+    // Update the option
+    // Maybe some options should be protected when user is connected (server,
+    // username, etc.).  And we should catch some options here, too
+    // (hide_offline_buddies for ex.)
+    if (!value) {
+      settings_del(stype, option);
+    } else {
+      settings_set(stype, option, value);
+    }
   }
 
-  assign = parse_assigment(arg, &option, &value);
-  if (!option) {
-    scr_LogPrint(LPRINT_NORMAL, "Set what option?");
-    return;
-  }
-  option_utf8 = to_utf8(option);
-  g_free(option);
-  if (!assign) {  // This is a query
-    const char *val = settings_opt_get(option_utf8);
-    if (val)
-      scr_LogPrint(LPRINT_NORMAL, "%s = [%s]", option_utf8, val);
-    else
-      scr_LogPrint(LPRINT_NORMAL, "Option %s is not set", option_utf8);
-    g_free(option_utf8);
-    return;
-  }
-  // Update the option
-  // Maybe some options should be protected when user is connected (server,
-  // username, etc.).  And we should catch some options here, too
-  // (hide_offline_buddies for ex.)
-  if (!value) {
-    settings_del(SETTINGS_TYPE_OPTION, option_utf8);
-  } else {
-    gchar *value_utf8 = to_utf8(value);
-    settings_set(SETTINGS_TYPE_OPTION, option_utf8, value_utf8);
-    g_free(value_utf8);
-    g_free(value);
-  }
-  g_free(option_utf8);
+  return NULL;
 }
 
 static void dump_alias(char *k, char *v, void *param)
@@ -2466,52 +3427,37 @@
   g_free(k_code);
 }
 
+#if 0
+
 static void do_rawxml(char *arg)
 {
-  char **paramlst;
-  char *subcmd;
+  cmdopts_t options = {
+    "rawxml",
+    NULL,
+    (cmdarg_t[1]){
+      { CMDOPT_REQUIRED | CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+    },
+    (cmdopts_t[1]){
+      { "send", NULL,
+        (cmdarg_t[1]){
+          { CMDOPT_REQUIRED | CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST,
+            { .arg = NULL } },
+        },
+        NULL },
+    },
+  };
 
   if (!xmpp_is_online()) {
     scr_LogPrint(LPRINT_NORMAL, "You are not connected.");
     return;
   }
 
-  paramlst = split_arg(arg, 2, 1); // subcmd, arg
-  subcmd = *paramlst;
-  arg = *(paramlst+1);
-
-  if (!subcmd || !*subcmd) {
-    scr_LogPrint(LPRINT_NORMAL, "Please read the manual page"
-                 " before using /rawxml :-)");
-    free_arg_lst(paramlst);
+  if (cmdopts_parse(arg, &options))
     return;
-  }
-
-  if (!strcasecmp(subcmd, "send"))  {
-    gchar *buffer;
-
-    if (!subcmd || !*subcmd) {
-      scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
-      free_arg_lst(paramlst);
-      return;
-    }
-
-    // We don't strip_arg_special_chars() here, because it would be a pain for
-    // the user to escape quotes in a XML stream...
-
-    buffer = to_utf8(arg);
-    if (buffer) {
-      scr_LogPrint(LPRINT_NORMAL, "Sending XML string");
-      lm_connection_send_raw(lconnection, buffer, NULL);
-      g_free(buffer);
-    } else {
-      scr_LogPrint(LPRINT_NORMAL, "Conversion error in XML string.");
-    }
-  } else {
-    scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
-  }
-
-  free_arg_lst(paramlst);
+
+  scr_LogPrint(LPRINT_NORMAL, "Sending XML string");
+  lm_connection_send_raw(lconnection, options.cmds[0].args[0].value.arg, NULL);
+  cmdopts_free(&options);
 }
 
 //  check_room_subcommand(arg, param_needed, buddy_must_be_a_room)
@@ -2815,6 +3761,8 @@
   free_arg_lst(paramlst);
 }
 
+#endif
+
 void cmd_room_leave(gpointer bud, char *arg)
 {
   gchar *roomid, *desc;
@@ -2833,6 +3781,8 @@
   g_free(roomid);
 }
 
+#if 0
+
 static void room_nick(gpointer bud, char *arg)
 {
   if (!buddy_getinsideroom(bud)) {
@@ -2874,7 +3824,7 @@
   fjid_utf8 = g_strdup_printf("%s/%s", buddy_getjid(bud), nick_utf8);
   g_free (nick_utf8);
   msg = to_utf8(arg);
-  send_message_to(fjid_utf8, msg, NULL, LM_MESSAGE_SUB_TYPE_NOT_SET, FALSE);
+  send_message_to(fjid_utf8, msg, NULL, msgtype_not_set, FALSE);
   g_free(fjid_utf8);
   g_free(msg);
   free_arg_lst(paramlst);
@@ -3052,6 +4002,8 @@
   free_arg_lst(paramlst);
 }
 
+#endif
+
 //  cmd_room_whois(..)
 // If interactive is TRUE, chatmode can be enabled.
 // Please note that usernick is expected in UTF-8 locale iff interactive is
@@ -3209,6 +4161,8 @@
   g_free (tmpnick);
 }
 
+#if 0
+
 static void display_all_bookmarks(void)
 {
   GSList *bm, *bmp;
@@ -3288,8 +4242,288 @@
 #endif
 }
 
+enum room_names_style_t {
+  room_names_style_normal = 0,
+  room_names_style_detail,
+  room_names_style_short,
+  room_names_style_quiet,
+  room_names_style_compact,
+};
+
+// room_names() is a variation of do_info(), for chatrooms only
+static void room_names(gpointer bud, enum room_names_style_t style)
+{
+  const char *bjid;
+  char *buffer;
+  GSList *resources, *p_res;
+
+  // Enter chat mode
+  scr_set_chatmode(TRUE);
+  scr_show_buddy_window();
+
+  bjid = buddy_getjid(bud);
+
+  buffer = g_slice_alloc(4096);
+  strncpy(buffer, "Room members:", 127);
+  scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
+
+  resources = buddy_getresources(bud);
+  for (p_res = resources ; p_res ; p_res = g_slist_next(p_res)) {
+    enum imstatus rstatus;
+    const char *rst_msg;
+
+    rstatus = buddy_getstatus(bud, p_res->data);
+    rst_msg = buddy_getstatusmsg(bud, p_res->data);
+
+    if (style == room_names_style_short) {
+      snprintf(buffer, 4095, "[%c] %s%s%s", imstatus2char[rstatus],
+               (char*)p_res->data,
+               rst_msg ? " -- " : "", rst_msg ? rst_msg : "");
+      scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
+    } else if (style == room_names_style_compact) {
+        enum imrole role = buddy_getrole(bud, p_res->data);
+        enum imaffiliation affil = buddy_getaffil(bud, p_res->data);
+        bool showaffil = (affil != affil_none);
+
+        snprintf(buffer, 4095, "[%c] %s (%s%s%s)",
+                 imstatus2char[rstatus], (char*)p_res->data,
+                 showaffil ? straffil[affil] : "\0",
+                 showaffil ? "/" : "\0",
+                 strrole[role]);
+        scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
+      } else {
+      // (Style "normal", "detail" or "quiet")
+      snprintf(buffer, 4095, "[%c] %s", imstatus2char[rstatus],
+               (char*)p_res->data);
+      scr_WriteIncomingMessage(bjid, buffer, 0, HBB_PREFIX_INFO, 0);
+      if (rst_msg && style != room_names_style_quiet) {
+        snprintf(buffer, 4095, "Status message: %s", rst_msg);
+        scr_WriteIncomingMessage(bjid, buffer,
+                                 0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
+      }
+      if (style == room_names_style_detail) {
+        enum imrole role = buddy_getrole(bud, p_res->data);
+        enum imaffiliation affil = buddy_getaffil(bud, p_res->data);
+
+        snprintf(buffer, 4095, "Role: %s", strrole[role]);
+        scr_WriteIncomingMessage(bjid, buffer,
+                                 0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
+        if (affil != affil_none) {
+          snprintf(buffer, 4095, "Affiliat.: %s", straffil[affil]);
+          scr_WriteIncomingMessage(bjid, buffer,
+                                   0, HBB_PREFIX_INFO | HBB_PREFIX_CONT, 0);
+        }
+      }
+    }
+    g_free(p_res->data);
+  }
+  g_slist_free(resources);
+  g_slice_free1(4096, buffer);
+}
+
 static void do_room(char *arg)
 {
+  enum room_scmd_t {
+    room_scmd_join, room_scmd_leave,
+    room_scmd_names,
+    room_scmd_nick,
+    room_scmd_privmsg,
+    room_scmd_remove,
+    room_scmd_topic,
+    room_scmd_unlock,
+    room_scmd_destroy,
+    room_scmd_whois,
+    room_scmd_ban, room_scmd_unban,
+    room_scmd_invite, room_scmd_kick,
+    room_scmd_role, room_scmd_affil,
+    room_scmd_setopt, room_scmd_bookmark,
+  } subcmd;
+  cmdopts_t options = {
+    "room",
+    (cmdopt_t[1]){
+      { CMDOPT_LAST, 'j', "jid", { .opt = NULL } },
+    },
+    (cmdarg_t[1]){
+      { CMDOPT_REQUIRED | CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+    },
+    (cmdopts_t[18]){
+      { "join", NULL,
+        (cmdarg_t[3]){
+          { 0,           { .arg = "." } },  // room jid
+          { 0,           { .arg = NULL } }, // nick
+          { CMDOPT_LAST, { .arg = NULL } }, // password
+        },
+        NULL, (gpointer)room_scmd_join, 0 },
+      { "leave", NULL,
+        (cmdarg_t[1]){
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_join, 0 },
+      { "names",
+        (cmdopt_t[4]){
+          { CMDOPT_SWITCH,               'd', "detail",  { .swc = 0 } },
+          { CMDOPT_SWITCH,               's', "short",   { .swc = 0 } },
+          { CMDOPT_SWITCH,               'q', "quiet",   { .swc = 0 } },
+          { CMDOPT_SWITCH | CMDOPT_LAST, 'c', "compact", { .swc = 0 } },
+        },
+        NULL, NULL, (gpointe)room_scmd_names, 0 },
+      { "nick", NULL,
+        (cmdarg_t[1]){
+          // nick
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_nick, 0 },
+      { "privmsg", NULL,
+        (cmdarg_t[2]) {
+          // nick
+          { CMDOPT_REQUIRED, { .arg = NULL } },
+          // message
+          { CMDOPT_REQUIRED | CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST,
+            { .arg = NULL } },
+        }
+        NULL, (gpointer)room_scmd_privmsg, 0 },
+      { "remove", NULL, NULL, NULL, (gpointer)room_scmd_remove, 0 },
+      { "topic", NULL,
+        (cmdarg_t[1]){
+          // topic
+          { CMDOPT_REQUIRED | CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST,
+            { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_topic, 0 },
+      { "unlock", NULL, NULL, NULL, (gpointer)room_scmd_unlock, 0 },
+      { "destroy", NULL,
+        (cmdarg_t[1]){
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_destroy, 0 },
+      { "whois", NULL,
+        (cmdarg_t[1]){
+          // nick
+          { CMDOPT_REQUIRED | CMDOPT_LAST, { .arg = NULL } },
+        }, NULL, (gpointer)room_scmd_whois, 0 },
+      { "ban", NULL,
+        (cmdarg_t[2]){
+          // nick
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        }, NULL, (gpointer)room_scmd_ban, 0 },
+      { "unban", NULL,
+        (cmdarg_t[1]){
+          // nick
+          { CMDOPT_REQUIRED | CMDOPT_LAST, { .arg = NULL } },
+        }, NULL, (gpointer)room_scmd_unban, 0 },
+      { "invite", NULL, // XXX [-d|direct] ?
+        (cmdarg_t[2]){
+          // jid
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_invite, 0 },
+      { "kick", NULL,
+        (cmdarg_t[2]){
+          // jid
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        },
+        NULL, (gpointer)room_scmd_kick, 0 },
+      { "role", NULL,
+        (cmdarg_t[3]){
+          // jid
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // role
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        }, NULL, (gpointer)room_scmd_role, 0 },
+      { "affil", NULL,
+        (cmdarg_t[3]){
+          // jid
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // affiliation
+          { CMDOPT_REQUIRED,                              { .arg = NULL } },
+          // reason
+          { CMDOPT_CATCHALL | CMDOPT_PLAIN | CMDOPT_LAST, { .arg = NULL } },
+        }, NULL, (gpointer)room_scmd_affil, 0 },
+      // XXX below goes some crazy subcommands stuff, that should be rather static lists
+      { "setopt", NULL,
+        (cmdarg_t[1]){
+          { CMDOPT_REQUIRED | CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+        },
+        (cmdopts_t[3]){
+          { "print_status", NULL,
+            (cmdarg_t[1]){
+              { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+            },
+            (cmdopts_t[4]){
+              { "default",    NULL, NULL, NULL, NULL, 0 },
+              { "none",       NULL, NULL, NULL, NULL, 0 },
+              { "in_and_out", NULL, NULL, NULL, NULL, 0 },
+              { "all",        NULL, NULL, NULL, NULL, CMDOPT_LAST },
+            }, NULL, 0 },
+          { "auto_whois", NULL,
+            (cmdarg_t[1]){
+              { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+            },
+            (cmdopts_t[3]){
+              { "default", NULL, NULL, NULL, NULL, 0 },
+              { "off",     NULL, NULL, NULL, NULL, 0 },
+              { "on",      NULL, NULL, NULL, NULL, CMDOPT_LAST },
+            }, NULL, 0 },
+          { "flag_joins", NULL,
+            (cmdarg_t[1]){
+              { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+            },
+            (cmdopts_t[4]){
+              { "default", NULL, NULL, NULL, NULL, 0 },
+              { "none",    NULL, NULL, NULL, NULL, 0 },
+              { "joins",   NULL, NULL, NULL, NULL, 0 },
+              { "all",     NULL, NULL, NULL, NULL, CMDOPT_LAST },
+            }, NULL, CMDOPT_LAST },
+        }, (gpointer)room_scmd_setopt, 0 },
+      { "bookmark", NULL,
+        (cmdarg_t[1]){
+          { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+        },
+        (cmdopts_t[2]){
+          { "add", NULL,
+            (cmdarg_t[1]){
+              { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+            },
+            (cmdopts_t[2]){
+              { "+autojoin", NULL,
+                (cmdarg_t[1]){
+                  // nick
+                  { CMDARG_LAST, { .arg = NULL } },
+                }, NULL, NULL, 0 },
+              { "-autojoin", NULL,
+                (cmdarg_t[1]){
+                  // nick
+                  { CMDARG_LAST, { .arg = NULL } },
+                }, NULL, NULL, CMDOPT_LAST },
+            }, NULL, 0 },
+          { "del", NULL,
+            (cmdarg_t[1]){
+              { CMDOPT_SUBCOMMAND | CMDOPT_LAST, { .cmd = NULL } },
+            },
+            (cmdopts_t[2]){
+              { "+autojoin", NULL,
+                (cmdarg_t[1]){
+                  // nick
+                  { CMDARG_LAST, { .arg = NULL } },
+                }, NULL, NULL, 0 },
+              { "-autojoin", NULL,
+                (cmdarg_t[1]){
+                  // nick
+                  { CMDARG_LAST, { .arg = NULL } },
+                }, NULL, NULL, CMDOPT_LAST },
+            }, NULL, 0 },
+        }, (gpointer)room_scmd_bookmark, CMDOPT_LAST },
+    },
+  };
   char **paramlst;
   char *subcmd;
   gpointer bud;
@@ -3347,7 +4581,7 @@
       cmd_room_leave(bud, arg);
   } else if (!strcasecmp(subcmd, "names"))  {
     if ((arg = check_room_subcommand(arg, FALSE, bud)) != NULL)
-      room_names(bud, arg);
+      room_names(bud, room_names_style_normal); // FIXME
   } else if (!strcasecmp(subcmd, "nick"))  {
     if ((arg = check_room_subcommand(arg, FALSE, bud)) != NULL)
       room_nick(bud, arg);
@@ -4162,5 +5396,6 @@
   }
   mcabber_set_terminate_ui();
 }
+#endif
 
 /* vim: set expandtab cindent cinoptions=>2\:2(0 sw=2 ts=2:  For Vim users... */
diff -r 1b0b563a81e6 mcabber/mcabber/commands.h
--- a/mcabber/mcabber/commands.h	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/commands.h	Wed May 15 12:48:30 2013 +0300
@@ -5,32 +5,394 @@
 
 #include <mcabber/config.h>
 
-// Command structure
+//
+//  DONE(?):
+//
+// * Clustering of options/switches
+//   - with separate values - just specify the same index
+//   - use the same flags on all of the clustered options, or result may be a bit unpredictable
+// * for now we'll do without multi-options.
+// * checks
+//   - argument type == public struct with name and callbacks, simple!
+//   - command checks should be just single callbacks
+//   - argument checker needs:
+//     - [in] value string (current or default)
+//     - [in] additional input data for some generic checkers (like string2enum wordlist)
+//     - [out] replacement value (not necessary string)
+//       - replacement value may need freeing
+//     - [out] error string
+//       - that may need freeing
+//
+//  TODO:
+//
+// * checkers can use visited flag to check if value is rw
+//   - but what if in-checker assigns ro value, like buddy_getjid()?
+//   - maybe need separate flag for that
+// * now, that we have rjid, merge bjid/fjid checkers into one
+// * Usable documentation in sources/howto
+// * Changelog entry
+// * --help
+//   - use argument type names and clarify, if needed in descriptions
+//   - command/subcommand descriptions
+//   - or maybe just do "/help foo"
+// * Add argument type field and subsystem to check values. Uses so far:
+//   - fjid (handle . and ./resource, perform check_jid_syntax())
+//   - bjid (handle ., strip resource, perform check_jid_syntax())
+//   - bjid in roster/buddylist (result - buddy?)
+//   - name in roster/buddylist (result - buddy?)
+//   - group in roster (result - buddy?)
+//   - other non-space string (strip trailing/starting spaces)
+//     - statusmask (restrictive, check symbols)
+//     - jidmask (non-restrictive, compl: inform what jids match)
+//     - search string (non-restrictive, compl: inform what matches, if possible)
+//     - rosternote (non-restrictive)
+//     - color (restrictive, semi-defined list)
+//     - nick (non-restrictive)
+//     - defined lists (restrictive)
+//     - date (restrictive)
+//     - percent (numerical, restrictive)
+//   - number (result - uint)
+//   - filename (expand_filename(), g_utf8_to_filename()?)
+//   - custom (maybe some helpers for constant stringlists)
+//   - text message (compl: nicks in rooms, dictionary?)
+// * Non-argument checks for commands/subcommands:
+//   - xmpp_is_online()
+//   - current_buddy
+// * commands:
+//   - say/msay/say_to/room privmsg/process_line() - sort things out, maybe write special argchecker
+//   - buffer close now only accepts windows with jid. additional checker, that also allows current 'status' buffer?
+// * Usable subsystem for completion, based on user-supplied completors
+// * Interface to history subsystem - command should have a way to inform, that it should not be stored in history.
+//
+//  XXX:
+//
+// * while escaped aliases should not be expanded, we should still unquote them for command search
+// * allow escaping in options?
+// * merge cmdopt_t and cmdarg_t?
+//   - improve the checker error messages
+//   - extra NULLs to specify in arg definition
+//   - inconvenient positioning of shortopt/longopt or argname in sequence
+//   - merge options/arguments?
+//   - describe values, not opt/args?
+// * command ref and name in special value 0 and indexing starts from 1?
+//   - requires allocation of values always
+//   - though there's not that many commands without arguments
+// * command checker is not quite effective
+//   - I'd like to run it before allocating anything
+//   - so far only one of them
+// * unable to put checker before allocation because in many cases it will be in subcommand.
+//   - separate command/subcommand values and use subcommand callbacks?
+//   - ignore command options and always go directly to subcommand parsing if present?
+//     (i.e. subcommand is always first argument and consumes the end of the line
+// * type name generator callback
+//   for generic types to construct something like "foo|bar|baz"
+// * [+foo|-foo] support?
+// * Subcommands with fallback to argument?
+// * Multivalue options
+//   - list
+//     - special checker
+//     - or run checker multiple times
+//   - or run command several times
+//     - what if there are several of such arguments?
+//
+
+//
+//  Type predeclarations
+//
+
+typedef struct cmdopts_struct      cmdopts_t;
+typedef struct cmdopt_struct       cmdopt_t;
+typedef struct cmdarg_struct       cmdarg_t;
+typedef struct cmdarg_type_struct  cmdarg_type_t;
+typedef struct cmdarg_value_struct cmdarg_value_t;
+
+//
+//  Callback type definitions
+//
+
+// Command execution environment check function.
+// Note: This is called before options are parsed, but values are already allocated.
+typedef gchar *(*cmd_checker_t)(const cmdopts_t *command, cmdarg_value_t *args);
+// Command function itself.
+// Command definition is provided for userdata access, should not be modified.
+typedef gchar *(*cmd_handler_t)(const cmdopts_t *command, cmdarg_value_t *args);
+// Should check value -> value.arg and replace, if needed.
+// Can set cmdarg_freeme flag to request type destructor call.
+// Can access argument definition via value -> src (but not modify it).
+typedef gchar *(*cmdarg_checker_t)(cmdarg_value_t *value);
+// Free resources, used in value, if needed.
+typedef void (*cmdarg_destructor_t)(cmdarg_value_t *value);
+// TODO Return possible completions for given argument.
+typedef void (*cmdarg_completor_t)(void);
+
+//
+//  Data type definitions
+//
+
+// command description flags
+typedef enum {
+  cmd_default     = 0x0000, // no flags
+  cmd_safe        = 0x0001, // command is safe to use in mcabberrc
+} cmd_flags_t;
+// argument description flags
+typedef enum {
+  cmdarg_default  = 0x0000, // no flags
+  cmdarg_catchall = 0x0001, // argument consumes the end of command line
+  cmdarg_plain    = 0x0002, // quotes and escapes are not processed
+  cmdarg_check    = 0x0004, // forse checker call on argument
+  cmdarg_required = 0x0008, // checker errors are fatal (checker may use as well)
+  cmdarg_subcmd   = 0x0010, // argument is subcommand
+  cmdarg_switch   = 0x0020, // option without argument, value is count
+  // shortcuts:
+  cmdarg_eol      = 0x0003, // catchall + plain
+  cmdarg_chreq    = 0x000C, // check + required
+  cmdarg_special  = 0x0030, // subcmd + switch
+  cmdarg_trigger  = 0x0024, // switch + check
+} cmdarg_flags_t;
+// argument value flags (internal)
+typedef enum {
+  cmdval_default  = 0x0000, // no flags
+  cmdval_visited  = 0x0100, // marks initialized arguments
+  cmdval_freeme   = 0x0200, // marks argument, that needs freeing
+} cmdval_flags_t;
+
+// command description
+struct cmdopts_struct {
+  const char          *name;     // [user,req] command name (error messages, help, subcommands)
+  const cmd_flags_t   flags;     // [user,req] safe
+  const cmd_checker_t check;     // [user,req] checker routine
+  const cmd_handler_t handle;    // [user,req] main command processing function
+  const cmdopt_t      *opts;     // [user,req] options
+  const cmdarg_t      *args;     // [user,req] arguments
+  const cmdopts_t     *cmds;     // [user,req] subcommands
+  gconstpointer       userdata;  // [user]
+  size_t              valno;     // internal, number of values to allocate
+};
+// positional/option argument description
+struct cmdarg_struct {
+  const char           *name;    // [user,req] argument name - errors, help (unused for switches, but must be initialized)
+  const size_t         pos;      // [user,req] value positional number
+  const cmdarg_flags_t flags;    // [user,req] catchall, plain, check, required, subcommand, switch
+  const char           *defval;  // [user,req] default value (unused for switches)
+  const cmdarg_type_t  *type;    // [user,req] type cbs - checker and completor (unused for switches and subcommands)
+  gconstpointer        chkdata;  // [user] instance data for type checker - eg string2enum list (unused for switches and subcommands)
+  gconstpointer        userdata; // [user]
+};
+// option description
+struct cmdopt_struct {
+  const char           shortopt; // [user,req]
+  const char           *longopt; // [user,req]
+  cmdarg_t             arg;      // [user,req]
+};
+// argument type description
+struct cmdarg_type_struct {
+  cmdarg_checker_t    check;     // [user,req] check string and set argument value
+  cmdarg_destructor_t free;      // [user,req] free argument value
+  cmdarg_completor_t  complete;  // [user,req]
+};
+// argument value
+struct cmdarg_value_struct {
+  const cmdarg_t *src;           // source of value
+  cmdval_flags_t flags;          // visited, freeme
+  union {                        // value:
+    unsigned long   uint;        // - unsigned integer
+    long            sint;        // - signed integer
+    guint           swc;         // - switch count
+    const gchar     *roarg;      // - XXX default value
+    gchar           *arg;        // - string argument
+    const cmdopts_t *cmd;        // - subcommand
+    struct {                     // - roster jid:
+      gpointer      bud;         //   - buddy struct
+      gchar         *resource;   //   - resource (optional)
+    } rjid;                      // 
+    struct {                     // - filename
+      gchar         *utf8;       //   - in utf8
+      gchar         *local;      //   - in local FS encoding
+    } fname;                     //
+    struct {                     // - key [= [value]]
+      gchar         *key;        //   - key
+      gchar         *value;      //   - value
+      gboolean      assignment;  //   - '=' was specified
+    } assign;                    //
+    time_t          time;        // - date/time
+    gpointer        ptr;         // - anything else
+  } value;                       //
+};
+
+//
+//  Public functions
+//
+
+// add command definition
+void cmd_define (cmdopts_t *command);
+// remove command definition
+void cmd_undef (cmdopts_t *command);
+
+//  error cmdopts_parse_argument ( startptr, endptr, flags )
+// Parses next argument according to flags and finishes it with zero.
+// Updates current/end pointers. Parsed string MUST be writable.
+// String may shrink in size (quotation/escapes), thus endpointer is also
+// updated.
+const char *cmdopts_parse_argument(gchar **pr, gchar **er, cmdarg_flags_t flags);
+
+// flags for cmd_execute()
+typedef enum {
+  cmdexe_default        = 0,    // no flags
+  cmdexe_check_safe     = 1<<0, // we're parsing main config file
+  cmdexe_check_verbatim = 1<<1, // check for verbatim input mode
+} cmdexe_flags_t;
+// return codes for cmd_execute() and process_line()
+typedef enum {
+  cmd_result_ok   = 0,       // all is ok
+  cmd_result_syntax_error,   // syntax or environment check error
+  cmd_result_value_error,    // argument value missing or type check error
+  cmd_result_not_found,      // no such command
+  cmd_result_runtime_error,  // error while executing command
+  cmd_result_verbatim,       // we're in verbatim mode and this is not "msay"
+  cmd_result_input,          // [process_line only] line was a message
+  cmd_result_quit = 255,     // this is hard-coded "quit" command
+} cmd_result_t;
+
+//  error cmd_execute ( command, flags )
+// Checks for "quit", "msay" in verbatim mode, expands aliases, checks command
+// for safety of execution, parses command's arguments according to definition,
+// executes command function. Returns cmdexe_result_ok (0) if all was ok.
+// Non-zero values mean, that command was not executed for some reason.
+// One special value is cmd_result_quit (255) - if it is "quit" command.
+cmd_result_t cmd_execute (gchar *commandline, cmdexe_flags_t flags);
+
+//  process_line(line)
+// Process a command/message line. If this isn't a command, this is a message
+// and it is sent to the currently selected buddy. The line is converted from
+// local encoding to utf8.
+// Returns 255 if the line is the /quit command, 0 on success and some other
+// error codes.
+cmd_result_t process_line(const char *line);
+
+//
+//  Command checkers
+//
+
+// checks if connection is available
+gchar *cmd_check_online (const cmdopts_t *command, cmdarg_value_t *values);
+gchar *cmd_check_current_buddy (const cmdopts_t *command, cmdarg_value_t *values);
+
+//
+//  Standard argument types
+//
+
+// enum for chkdata for roster/jid checkers
+typedef enum {
+  cmdarg_roster_notset    = 0x0000,
+  cmdarg_roster_user      = ROSTER_TYPE_USER,    // 0x0001
+  cmdarg_roster_group     = ROSTER_TYPE_GROUP,   // 0x0002
+  cmdarg_roster_agent     = ROSTER_TYPE_AGENT,   // 0x0004
+  cmdarg_roster_room      = ROSTER_TYPE_ROOM,    // 0x0008
+  cmdarg_roster_special   = ROSTER_TYPE_SPECIAL, // 0x0010
+  cmdarg_roster_name      = 0x0100,              // namesearch
+  cmdarg_roster_activeres = 0x0200,              // active resource
+  cmdarg_roster_getgroup  = 0x0400,              // group of buddy
+  // shortcuts:
+  cmdarg_roster_buddy     = 0x0005, // user + agent (single buddy)
+  cmdarg_roster_entity    = 0x000D, // user + agent + room (xmpp entity)
+  cmdarg_roster_buffer    = 0x001D, // user + agent + room + special (have buffer)
+  cmdarg_roster_normal    = 0x000F, // user + group + agent + room (not special)
+  cmdarg_roster_all       = 0x001F, // user + group + agent + room + special
+  cmdarg_roster_grouponly = 0x0402, // group + getgroup (search group buddy)
+  cmdarg_roster_mask      = 0x001F, // all
+} cmdarg_roster_t;
+
+// array entry for chkdata for string2enum checker
 typedef struct {
-  char name[32];
-  const char *help;
-  guint completion_flags[2];
-  void (*func)(char *);
-  gpointer userdata;
-} cmd;
+  const char *name;
+  int        value;
+} string2enum_t;
 
-void cmd_init(void);
-cmd *cmd_get(const char *command);
-int  process_line(const char *line);
-int  process_command(const char *line, guint iscmd);
-char *expandalias(const char *line);
-#ifdef MODULES_ENABLE
-gpointer cmd_del(gpointer id);
-gpointer cmd_add(const char *name, const char *help, guint flags1, guint flags2,
-                 void (*f)(char*), gpointer userdata);
-gboolean cmd_set_safe(const gchar *name, gboolean safe);
+// destructor, that g_free()s value.arg
+void cmdarg_free_gfree (cmdarg_value_t *arg);
+
+// strips space at the start/end and ensures, that string have non-zero length
+gchar *cmdarg_check_nonspace        (cmdarg_value_t *arg);
+// checks, that jid is in roster and returns buddy
+gchar *cmdarg_check_roster_bjid     (cmdarg_value_t *arg);
+// checks, that name is in roster and returns buddy
+gchar *cmdarg_check_roster_name     (cmdarg_value_t *arg);
+// checks, that jid is in roster and have specified resource, returns buddy and resource
+gchar *cmdarg_check_roster_resource (cmdarg_value_t *arg);
+// checks for group with given name and returns buddy
+gchar *cmdarg_check_roster_group    (cmdarg_value_t *arg);
+// checks correctness of jid syntax
+gchar *cmdarg_check_fjid            (cmdarg_value_t *arg);
+// checks correctness of jid syntax, strips resource
+gchar *cmdarg_check_bjid            (cmdarg_value_t *arg);
+// transforms to guint
+gchar *cmdarg_check_uint            (cmdarg_value_t *arg);
+// checks, that string consists of characters from a given set
+gchar *cmdarg_check_charset         (cmdarg_value_t *arg);
+// checks, that string is from the given list, and returns corresponding guint value
+gchar *cmdarg_check_string2enum     (cmdarg_value_t *arg);
+// checks mcabber color name syntax
+gchar *cmdarg_check_color           (cmdarg_value_t *arg);
+// expands filename and converts encoding
+gchar *cmdarg_check_filename        (cmdarg_value_t *arg);
+// destructor, frees both names
+void cmdarg_free_fname              (cmdarg_value_t *arg);
+// converts iso-8601 date to epoch
+gchar *cmdarg_check_date            (cmdarg_value_t *arg);
+
+// ready for use standard type descriptions
+const cmdarg_type_t cmdarg_type_nonspace;
+const cmdarg_type_t cmdarg_type_roster_bjid;
+const cmdarg_type_t cmdarg_type_roster_name;
+const cmdarg_type_t cmdarg_type_roster_resource;
+const cmdarg_type_t cmdarg_type_roster_group;
+const cmdarg_type_t cmdarg_type_fjid;
+const cmdarg_type_t cmdarg_type_bjid;
+const cmdarg_type_t cmdarg_type_uint;
+const cmdarg_type_t cmdarg_type_charset;
+const cmdarg_type_t cmdarg_type_string2enum;
+const cmdarg_type_t cmdarg_type_color;
+const cmdarg_type_t cmdarg_type_filename;
+const cmdarg_type_t cmdarg_type_date;
+
+//
+//  Private
+//
+
+void cmd_init (void);
+void cmd_uninit (void);
+
+#if 0
+// return highest index for value, used in command
+size_t cmdopts_count_values (const cmdopts_t *command);
+
+// allocate values array to store arguments
+cmdarg_value_t *cmdopts_allocate_values (const cmdopts_t *command);
+
+//  error cmdopts_parse_internal ( startptr, endptr, commanddef, values )
+// Parses command arguments according to command definition.
+// Parsed string MUST be writable. Regardless of success or error, input
+// string should be considered corrupted by parsing process.
+gchar *cmdopts_parse_internal (gchar **pr, gchar **er, const cmdopts_t *command, cmdarg_value_t *values);
+
+// perform type checking/conversion on parsed values
+gchar *cmdopts_check_values (const cmdopts_t *command, cmdarg_value_t *values);
+
+//  cmdopts_free_values ( commanddef, values )
+// Free allocated by checkers resources and values array.
+void cmdopts_free_values (const cmdopts_t *command, cmdarg_value_t *values);
+
+typedef enum {
+  msgtype_not_set,
+  msgtype_normal,
+  msgtype_headline,
+} msgtype_t;
+
+void say_cmd(char *arg, msgtype_t msgtype);
 #endif
-gboolean cmd_is_safe(const gchar *name);
 
 void cmd_room_whois(gpointer bud, const char *nick, guint interactive);
 void cmd_room_leave(gpointer bud, char *arg);
-void cmd_setstatus(const char *recipient, const char *arg);
-void say_cmd(char *arg, int parse_flags);
 
 #endif /* __MCABBER_COMMANDS_H__ */
 
diff -r 1b0b563a81e6 mcabber/mcabber/hooks.c
--- a/mcabber/mcabber/hooks.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/hooks.c	Wed May 15 12:48:30 2013 +0300
@@ -638,10 +638,9 @@
 
   scr_LogPrint(LPRINT_LOGNORM, "Running hook-post-connect...");
 
-  cmdline = from_utf8(hook_command);
-  if (process_command(cmdline, TRUE) == 255)
+  cmdline = g_strdup(hook_command);
+  if (cmd_execute(cmdline, cmd_default) == cmd_result_quit)
     mcabber_set_terminate_ui();
-
   g_free(cmdline);
 }
 
@@ -665,10 +664,9 @@
 
   scr_LogPrint(LPRINT_LOGNORM, "Running hook-pre-disconnect...");
 
-  cmdline = from_utf8(hook_command);
-  if (process_command(cmdline, TRUE) == 255)
+  cmdline = g_strdup(hook_command);
+  if (cmd_execute(cmdline, cmd_default) == cmd_result_quit)
     mcabber_set_terminate_ui();
-
   g_free(cmdline);
 }
 
diff -r 1b0b563a81e6 mcabber/mcabber/roster.c
--- a/mcabber/mcabber/roster.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/roster.c	Wed May 15 12:48:30 2013 +0300
@@ -1586,13 +1586,14 @@
 // Look for a buddy whose name or jid contains string.
 // Search begins at current_buddy; if no match is found in the the buddylist,
 // return NULL;
+// Note: before this function considered its argument to be in local encoding,
+//       now argument must be in utf8.
 GList *buddy_search(char *string)
 {
   GList *buddy = current_buddy;
   roster *roster_usr;
   if (!buddylist || !current_buddy) return NULL;
   for (;;) {
-    gchar *jid_locale, *name_locale;
     char *found = NULL;
 
     buddy = g_list_next(buddy);
@@ -1601,17 +1602,13 @@
 
     roster_usr = (roster*)buddy->data;
 
-    jid_locale = from_utf8(roster_usr->jid);
-    if (jid_locale) {
-      found = strcasestr(jid_locale, string);
-      g_free(jid_locale);
+    if (roster_usr->jid) {
+      found = strcasestr(roster_usr->jid, string);
       if (found)
         return buddy;
     }
-    name_locale = from_utf8(roster_usr->name);
-    if (name_locale) {
-      found = strcasestr(name_locale, string);
-      g_free(name_locale);
+    if (roster_usr->name) {
+      found = strcasestr(roster_usr->name, string);
       if (found)
         return buddy;
     }
diff -r 1b0b563a81e6 mcabber/mcabber/screen.c
--- a/mcabber/mcabber/screen.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/screen.c	Wed May 15 12:48:30 2013 +0300
@@ -3630,7 +3630,7 @@
 {
   scr_check_auto_away(TRUE);
   last_activity_buddy = current_buddy;
-  if (process_line(inputLine))
+  if (process_line(inputLine) == cmd_result_quit)
     return 255;
   // Add line to history
   scr_cmdhisto_addline(inputLine);
@@ -3802,8 +3802,12 @@
 void readline_send_multiline(void)
 {
   // Validate current multi-line
-  if (multimode)
-    process_command(mkcmdstr("msay send"), TRUE);
+  if (multimode) {
+    // FIXME - do it without parsing
+    gchar *cmdline = g_strdup("msay send");
+    cmd_execute(cmdline, cmdexe_default);
+    g_free(cmdline);
+  }
 }
 
 void readline_insert(const char *toinsert)
@@ -3883,6 +3887,8 @@
 // Initiate or continue a completion...
 static void scr_handle_tab(void)
 {
+// FIXME completion
+#if 0
   int nrow;
   const char *row;
   const char *cchar;
@@ -3978,6 +3984,7 @@
     if (cchar)
       scr_insert_text(cchar);
   }
+#endif
 }
 
 static void scr_cancel_current_completion(void)
@@ -4098,7 +4105,9 @@
 {
   if (!Curses) return;
   // Leave multi-line mode
-  process_command(mkcmdstr("msay abort"), TRUE);
+  gchar *cmdline = g_strdup("msay abort");
+  cmd_execute(cmdline, cmdexe_default);
+  g_free(cmdline);
   // Same as Ctrl-g, now
   scr_cancel_current_completion();
   scr_end_current_completion();
@@ -4302,10 +4311,12 @@
   boundcmd = settings_get(SETTINGS_TYPE_BINDING, asciikey);
 
   if (boundcmd) {
-    gchar *cmdline = from_utf8(boundcmd);
+    gchar *cmdline = g_strdup(boundcmd);
     scr_check_auto_away(TRUE);
-    if (process_command(cmdline, TRUE))
+    if (cmd_execute(cmdline, cmdexe_default) == cmd_result_quit) {
+      g_free(cmdline);
       return 255; // Quit
+    }
     g_free(cmdline);
     return 0;
   }
diff -r 1b0b563a81e6 mcabber/mcabber/settings.c
--- a/mcabber/mcabber/settings.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/settings.c	Wed May 15 12:48:30 2013 +0300
@@ -183,28 +183,12 @@
     if ((*line == '\n') || (*line == '\0') || (*line == '#'))
       continue;
 
-    // If we aren't in runtime (i.e. startup) we'll only accept "safe" commands
-    if (!runtime) {
-      const gchar *cmdend = strchr(line, ' ');
-      gchar *cmdname = NULL;
-      gboolean safe;
-      if (cmdend)
-        cmdname = g_strndup(line, cmdend - line);
-      safe = cmd_is_safe(cmdname ? cmdname : line);
-      g_free(cmdname);
-      if (!safe) {
-        scr_log_print(LPRINT_LOGNORM, "Error in configuration file (l. %d): "
-                      "this command can't be used here", ln);
-        err++;
-        continue;
-      }
-    }
-
-    // Set the leading COMMAND_CHAR to build a command line
-    // and process the command
-    *(--line) = COMMAND_CHAR;
-    if (process_command(line, TRUE) == 255)
+    // FIXME: in-buffer conversion? some glib iochannel stuff?
+    gchar *utf8 = to_utf8(line);
+    if (cmd_execute(utf8, runtime ? cmdexe_default : cmdexe_check_safe)
+        == cmd_result_quit)
       mcabber_set_terminate_ui();
+    g_free(utf8);
   }
   g_free(buf);
   fclose(fp);
diff -r 1b0b563a81e6 mcabber/mcabber/xmpp_iq.c
--- a/mcabber/mcabber/xmpp_iq.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/mcabber/xmpp_iq.c	Wed May 15 12:48:30 2013 +0300
@@ -71,20 +71,20 @@
 struct adhoc_status {
   char *name;   // the name used by adhoc
   char *description;
-  char *status; // the string, used by setstus
+  enum imstatus status; // corresponding imstatus value
 };
 // It has to match imstatus of roster.h!
 const struct adhoc_status adhoc_status_list[] = {
-  {"offline", "Offline", "offline"},
-  {"online", "Online", "avail"},
-  {"chat", "Chat", "free"},
-  {"dnd", "Do not disturb", "dnd"},
-  {"xa", "Extended away", "notavail"},
-  {"away", "Away", "away"},
+  {"offline", "Offline", offline},
+  {"online", "Online", available},
+  {"chat", "Chat", freeforchat},
+  {"dnd", "Do not disturb", dontdisturb},
+  {"xa", "Extended away", notavail},
+  {"away", "Away", away},
 #ifdef WITH_DEPRECATED_STATUS_INVISIBLE
-  {"invisible", "Invisible", "invisible"},
+  {"invisible", "Invisible", invisible},
 #endif
-  {NULL, NULL, NULL},
+  {NULL, NULL, imstatus_size},
 };
 
 static char *generate_session_id(char *prefix)
@@ -289,10 +289,11 @@
       if (value) {
         for (s = adhoc_status_list; !s->name || strcmp(s->name, value); s++);
         if (s->name) {
-          char *status = g_strdup_printf("%s %s", s->status,
-                                         message ? message : "");
-          cmd_setstatus(NULL, status);
-          g_free(status);
+          // It makes sense to reset autoaway before changing the status
+          // (esp. for FIFO or remote commands) or the behaviour could be
+          // unexpected...
+          scr_check_auto_away(TRUE);
+          xmpp_setstatus(s->status, NULL, message ? message : "", FALSE);
           lm_message_node_set_attribute(command, "status", "completed");
           lm_message_node_add_dataform_result(command,
                                               "Status has been changed");
diff -r 1b0b563a81e6 mcabber/modules/beep/beep.c
--- a/mcabber/modules/beep/beep.c	Wed Mar 13 16:11:16 2013 +0200
+++ b/mcabber/modules/beep/beep.c	Wed May 15 12:48:30 2013 +0300
@@ -31,6 +31,7 @@
 
 static void beep_init   (void);
 static void beep_uninit (void);
+static gchar *do_beep (const cmdopts_t *command, cmdarg_value_t *values);
 
 /* Module description */
 module_info_t info_beep = {
@@ -46,9 +47,7 @@
         .next            = NULL,
 };
 
-static guint    beep_cid  = 0;  /* Command completion category id */
-static gpointer beep_cmid = 0;  /* Command id */
-static guint    beep_hid  = 0;  /* Hook handler id */
+static guint beep_hid  = 0;  /* Hook handler id */
 
 /* Event handler */
 static guint beep_hh(const gchar *hookname, hk_arg_t *args, gpointer userdata)
@@ -61,20 +60,42 @@
   return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
 }
 
+static const string2enum_t s2e_onoff[] = {
+  { "on",      1  },
+  { "off",     0  },
+  { "enable",  1  },
+  { "disable", 0  },
+  { "1",       1  },
+  { "0",       0  },
+  { "show",    2  },
+  { NULL,      0  },
+};
+
+typedef enum {
+  pos_beep_enable = 0,
+} pos_beep_t;
+
+static cmdopts_t def_beep = {
+  "beep",
+  cmd_default,
+  NULL,
+  do_beep,
+  NULL,
+  (cmdarg_t[2]){
+    {"on|off", pos_beep_enable, cmdarg_chreq, "show", &cmdarg_type_string2enum, (gpointer)s2e_onoff},
+    {NULL}
+  },
+  NULL
+};
+
 /* beep command handler */
-static void do_beep(char *args)
+static gchar *do_beep(const cmdopts_t *command, cmdarg_value_t *values)
 {
   /* Check arguments, and if recognized,
    * set mcabber option accordingly */
-  if (!strcmp(args, "enable") ||
-      !strcmp(args, "on") ||
-      !strcmp(args, "yes") ||
-      !strcmp(args, "1"))
+  if (values[pos_beep_enable].value.uint == 1)
     settings_set(SETTINGS_TYPE_OPTION, "beep_enable", "1");
-  else if (!strcmp(args, "disable") ||
-           !strcmp(args, "off") ||
-           !strcmp(args, "no") ||
-           !strcmp(args, "0"))
+  else if (values[pos_beep_enable].value.uint == 0)
     settings_set(SETTINGS_TYPE_OPTION, "beep_enable", "0");
 
   /* Output current state, either if state is
@@ -83,19 +104,14 @@
     scr_log_print(LPRINT_NORMAL, "Beep on messages is enabled");
   else
     scr_log_print(LPRINT_NORMAL, "Beep on messages is disabled");
+  return NULL;
 }
 
 /* Initialization */
 static void beep_init(void)
 {
-  /* Create completions */
-  beep_cid = compl_new_category(COMPL_FLAGS_SORT);
-  if (beep_cid) {
-    compl_add_category_word(beep_cid, "enable");
-    compl_add_category_word(beep_cid, "disable");
-  }
   /* Add command */
-  beep_cmid = cmd_add("beep", "", beep_cid, 0, do_beep, NULL);
+  cmd_define(&def_beep);
   /* Add handler
    * We are only interested in incoming message events
    */
@@ -109,11 +125,7 @@
   /* Unregister event handler */
   hk_del_handler(HOOK_POST_MESSAGE_IN, beep_hid);
   /* Unregister command */
-  cmd_del(beep_cmid);
-  /* Give back completion handle */
-  if (beep_cid)
-    compl_del_category(beep_cid);
+  cmd_undef(&def_beep);
 }
 
-/* The End */
 /* vim: set et cindent cinoptions=>2\:2(0 ts=2 sw=2:  For Vim users... */
