Revert "Revert "re_order_kwargs, default_arg_delim, create_command_string functions""
This reverts commit 65b0a85ef5218a40df1430363f301faa6526033d.
Change-Id: I9ab2039bd2cc2604a537a8f660eb25b0c10ed8c7
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