re_order_kwargs, default_arg_delim, create_command_string functions

New functions:

re_order_kwargs:
 - Re-order the kwargs to match the order in which they were specified on a
   function invocation and return as an ordered dictionary.
default_arg_delim:
 - Return the default argument delimiter value for the given arg_dashes value.
create_command_string:
 - Create and return a bash command string consisting of the given arguments
   formatted as text.

Change-Id: I77c0183444c90c395e7e95d27bbbe62a556aa560
Signed-off-by: Michael Walsh <micwalsh@us.ibm.com>
diff --git a/lib/gen_cmd.py b/lib/gen_cmd.py
index 6f88be1..32f515d 100644
--- a/lib/gen_cmd.py
+++ b/lib/gen_cmd.py
@@ -10,6 +10,8 @@
 import collections
 import signal
 import time
+import re
+import inspect
 
 import gen_print as gp
 import gen_valid as gv
@@ -518,3 +520,266 @@
     kwargs['test_mode'] = test_mode
 
     return shell_cmd(command_string, **kwargs)
+
+
+def re_order_kwargs(stack_frame_ix, **kwargs):
+    r"""
+    Re-order the kwargs to match the order in which they were specified on a
+    function invocation and return as an ordered dictionary.
+
+    Note that this re_order_kwargs function should not be necessary in python
+    versions 3.6 and beyond.
+
+    Example:
+
+    The caller calls func1 like this:
+
+    func1('mike', arg1='one', arg2='two', arg3='three')
+
+    And func1 is defined as follows:
+
+    def func1(first_arg, **kwargs):
+
+        kwargs = re_order_kwargs(first_arg_num=2, stack_frame_ix=3, **kwargs)
+
+    The kwargs dictionary before calling re_order_kwargs (where order is not
+    guaranteed):
+
+    kwargs:
+      kwargs[arg3]:          three
+      kwargs[arg2]:          two
+      kwargs[arg1]:          one
+
+    The kwargs dictionary after calling re_order_kwargs:
+
+    kwargs:
+      kwargs[arg1]:          one
+      kwargs[arg2]:          two
+      kwargs[arg3]:          three
+
+    Note that the re-ordered kwargs match the order specified on the call to
+    func1.
+
+    Description of argument(s):
+    stack_frame_ix                  The stack frame of the function whose
+                                    kwargs values must be re-ordered.  0 is
+                                    the stack frame of re_order_kwargs, 1 is
+                                    the stack from of its caller and so on.
+    kwargs                          The keyword argument dictionary which is
+                                    to be re-ordered.
+    """
+
+    new_kwargs = collections.OrderedDict()
+
+    # Get position number of first keyword on the calling line of code.
+    (args, varargs, keywords, locals) =\
+        inspect.getargvalues(inspect.stack()[stack_frame_ix][0])
+    first_kwarg_pos = 1 + len(args)
+    if varargs is not None:
+        first_kwarg_pos += len(locals[varargs])
+    for arg_num in range(first_kwarg_pos, first_kwarg_pos + len(kwargs)):
+        # This will result in an arg_name value such as "arg1='one'".
+        arg_name = gp.get_arg_name(None, arg_num, stack_frame_ix + 2)
+        # Continuing with the prior example, the following line will result
+        # in key being set to 'arg1'.
+        key = arg_name.split('=')[0]
+        new_kwargs[key] = kwargs[key]
+
+    return new_kwargs
+
+
+def default_arg_delim(arg_dashes):
+    r"""
+    Return the default argument delimiter value for the given arg_dashes value.
+
+    Note: this function is useful for functions that manipulate bash command
+    line arguments (e.g. --parm=1 or -parm 1).
+
+    Description of argument(s):
+    arg_dashes                      The argument dashes specifier (usually,
+                                    "-" or "--").
+    """
+
+    if arg_dashes == "--":
+        return "="
+
+    return " "
+
+
+def create_command_string(command, *pos_parms, **options):
+    r"""
+    Create and return a bash command string consisting of the given arguments
+    formatted as text.
+
+    The default formatting of options is as follows:
+
+    <single dash><option name><space delim><option value>
+
+    Example:
+
+    -parm value
+
+    The caller can change the kind of dashes/delimiters used by specifying
+    "arg_dashes" and/or "arg_delims" as options.  These options are processed
+    specially by the create_command_string function and do NOT get inserted
+    into the resulting command string.  All options following the
+    arg_dashes/arg_delims options will then use the specified values for
+    dashes/delims.  In the special case of arg_dashes equal to "--", the
+    arg_delim will automatically be changed to "=".  See examples below.
+
+    Quoting rules:
+
+    The create_command_string function will single quote option values as
+    needed to prevent bash expansion.  If the caller wishes to defeat this
+    action, they may single or double quote the option value themselves.  See
+    examples below.
+
+    pos_parms are NOT automatically quoted.  The caller is advised to either
+    explicitly add quotes or to use the quote_bash_parm functions to quote any
+    pos_parms.
+
+    Examples:
+
+    command_string = create_command_string('cd', '~')
+
+    Result:
+    cd ~
+
+    Note that the pos_parm ("~") does NOT get quoted, as per the
+    aforementioned rules.  If quotes are desired, they may be added explicitly
+    by the caller:
+
+    command_string = create_command_string('cd', '\'~\'')
+
+    Result:
+    cd '~'
+
+    command_string = create_command_string('grep', '\'^[^ ]*=\'',
+        '/tmp/myfile', i=None, m='1', arg_dashes='--', color='always')
+
+    Result:
+    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
+
+    In the preceding example, note the use of None to cause the "i" parm to be
+    treated as a flag (i.e. no argument value is generated).  Also, note the
+    use of arg_dashes to change the type of dashes used on all subsequent
+    options.  The following example is equivalent to the prior.  Note that
+    quote_bash_parm is used instead of including the quotes explicitly.
+
+    command_string = create_command_string('grep', quote_bash_parm('^[^ ]*='),
+        '/tmp/myfile', i=None,  m='1', arg_dashes='--', color='always')
+
+    Result:
+    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
+
+    In the following example, note the automatic quoting of the password
+    option, as per the aforementioned rules.
+
+    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
+        m='1', arg_dashes='--', password='${my_pw}')
+
+    However, let's say that the caller wishes to have bash expand the password
+    value.  To achieve this, the caller can use double quotes:
+
+    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
+        m='1', arg_dashes='--', password='"${my_pw}"')
+
+    Result:
+    my_pgm -i -m 1 --password="${my_pw}" /tmp/myfile
+
+    command_string = create_command_string('ipmitool', 'power status',
+        I='lanplus', C='3', U='root', P='0penBmc', H='wsbmc010')
+
+    Result:
+    ipmitool -I lanplus -C 3 -U root -P 0penBmc -H wsbmc010 power status
+
+    By default create_command_string will take measures to preserve the order
+    of the callers options.  In some cases, this effort may fail (as when
+    calling directly from a robot program).  In this case, the caller can
+    accept the responsibility of keeping an ordered list of options by calling
+    this function with the last positional parm as some kind of dictionary
+    (preferably an OrderedDict) and avoiding the use of any actual option args.
+
+    Example:
+    kwargs = collections.OrderedDict([('pass', 0), ('fail', 0)])
+    command_string = create_command_string('my program', 'pos_parm1', kwargs)
+
+    Result:
+
+    my program -pass 0 -fail 0 pos_parm1
+
+    Note to programmers who wish to write a wrapper to this function:  To get
+    the options to be processed correctly, the wrapper function must include a
+    _stack_frame_ix_ keyword argument to allow this function to properly
+    re-order options:
+
+    def create_ipmi_ext_command_string(command, **kwargs):
+
+        return create_command_string('ipmitool', command, _stack_frame_ix_=2,
+            **kwargs)
+
+    Example call of wrapper function:
+
+    command_string = create_ipmi_ext_command_string('power status',
+    I='lanplus')
+
+    Description of argument(s):
+    command                         The command (e.g. "cat", "sort",
+                                    "ipmitool", etc.).
+    pos_parms                       The positional parms for the command (e.g.
+                                    PATTERN, FILENAME, etc.).  These will be
+                                    placed at the end of the resulting command
+                                    string.
+    options                         The command options (e.g. "-m 1",
+                                    "--max-count=NUM", etc.).  Note that if
+                                    the value of any option is None, then it
+                                    will be understood to be a flag (for which
+                                    no value is required).
+    """
+
+    arg_dashes = "-"
+    delim = default_arg_delim(arg_dashes)
+
+    command_string = command
+
+    if gp.is_dict(pos_parms[-1]):
+        # Convert pos_parms from tuple to list.
+        pos_parms = list(pos_parms)
+        # Re-assign options to be the last pos_parm value (which is a
+        # dictionary).
+        options = pos_parms[-1]
+        # Now delete the last pos_parm.
+        del pos_parms[-1]
+    else:
+        # Either get stack_frame_ix from the caller via options or set it to
+        # the default value.
+        if '_stack_frame_ix_' in options:
+            stack_frame_ix = options['_stack_frame_ix_']
+            del options['_stack_frame_ix_']
+        else:
+            stack_frame_ix = 1
+        # Re-establish the original options order as specified on the
+        # original line of code.  This function depends on correct order.
+        options = re_order_kwargs(stack_frame_ix, **options)
+    for key, value in options.items():
+        # Check for special values in options and process them.
+        if key == "arg_dashes":
+            arg_dashes = str(value)
+            delim = default_arg_delim(arg_dashes)
+            continue
+        if key == "arg_delim":
+            delim = str(value)
+            continue
+        # Format the options elements into the command string.
+        command_string += " " + arg_dashes + key
+        if value is not None:
+            command_string += delim
+            if re.match(r'^(["].*["]|[\'].*[\'])$', str(value)):
+                # Already quoted.
+                command_string += str(value)
+            else:
+                command_string += gm.quote_bash_parm(str(value))
+    # Finally, append the pos_parms to the end of the command_string.
+    command_string = command_string + ' ' + ' '.join(pos_parms)
+
+    return command_string