1#!/usr/bin/env python3
2
3r"""
4This module provides command execution functions such as cmd_fnc and cmd_fnc_u.
5"""
6
7import collections
8import inspect
9import os
10import re
11import signal
12import subprocess
13import sys
14import time
15
16import func_args as fa
17import gen_misc as gm
18import gen_print as gp
19import gen_valid as gv
20
21robot_env = gp.robot_env
22
23if robot_env:
24    from robot.libraries.BuiltIn import BuiltIn
25
26
27# cmd_fnc and cmd_fnc_u should now be considered deprecated.  shell_cmd and t_shell_cmd should be used
28# instead.
29def cmd_fnc(
30    cmd_buf,
31    quiet=None,
32    test_mode=None,
33    debug=0,
34    print_output=1,
35    show_err=1,
36    return_stderr=0,
37    ignore_err=1,
38):
39    r"""
40    Run the given command in a shell and return the shell return code and the output.
41
42    Description of arguments:
43    cmd_buf                         The command string to be run in a shell.
44    quiet                           Indicates whether this function should run the print_issuing() function
45                                    which prints "Issuing: <cmd string>" to stdout.
46    test_mode                       If test_mode is set, this function will not actually run the command.  If
47                                    print_output is set, it will print "(test_mode) Issuing: <cmd string>" to
48                                    stdout.
49    debug                           If debug is set, this function will print extra debug info.
50    print_output                    If this is set, this function will print the stdout/stderr generated by
51                                    the shell command.
52    show_err                        If show_err is set, this function will print a standardized error report
53                                    if the shell command returns non-zero.
54    return_stderr                   If return_stderr is set, this function will process the stdout and stderr
55                                    streams from the shell command separately.  It will also return stderr in
56                                    addition to the return code and the stdout.
57    """
58
59    # Determine default values.
60    quiet = int(gm.global_default(quiet, 0))
61    test_mode = int(gm.global_default(test_mode, 0))
62
63    if debug:
64        gp.print_vars(cmd_buf, quiet, test_mode, debug)
65
66    err_msg = gv.valid_value(cmd_buf)
67    if err_msg != "":
68        raise ValueError(err_msg)
69
70    if not quiet:
71        gp.pissuing(cmd_buf, test_mode)
72
73    if test_mode:
74        if return_stderr:
75            return 0, "", ""
76        else:
77            return 0, ""
78
79    if return_stderr:
80        err_buf = ""
81        stderr = subprocess.PIPE
82    else:
83        stderr = subprocess.STDOUT
84
85    sub_proc = subprocess.Popen(
86        cmd_buf,
87        bufsize=1,
88        shell=True,
89        universal_newlines=True,
90        executable="/bin/bash",
91        stdout=subprocess.PIPE,
92        stderr=stderr,
93    )
94    out_buf = ""
95    if return_stderr:
96        for line in sub_proc.stderr:
97            try:
98                err_buf += line
99            except TypeError:
100                line = line.decode("utf-8")
101                err_buf += line
102            if not print_output:
103                continue
104            gp.gp_print(line)
105    for line in sub_proc.stdout:
106        try:
107            out_buf += line
108        except TypeError:
109            line = line.decode("utf-8")
110            out_buf += line
111        if not print_output:
112            continue
113        gp.gp_print(line)
114    if print_output and not robot_env:
115        sys.stdout.flush()
116    sub_proc.communicate()
117    shell_rc = sub_proc.returncode
118    if shell_rc != 0:
119        err_msg = "The prior shell command failed.\n"
120        err_msg += gp.sprint_var(shell_rc, gp.hexa())
121        if not print_output:
122            err_msg += "out_buf:\n" + out_buf
123
124        if show_err:
125            gp.print_error_report(err_msg)
126        if not ignore_err:
127            if robot_env:
128                BuiltIn().fail(err_msg)
129            else:
130                raise ValueError(err_msg)
131
132    if return_stderr:
133        return shell_rc, out_buf, err_buf
134    else:
135        return shell_rc, out_buf
136
137
138def cmd_fnc_u(
139    cmd_buf,
140    quiet=None,
141    debug=None,
142    print_output=1,
143    show_err=1,
144    return_stderr=0,
145    ignore_err=1,
146):
147    r"""
148    Call cmd_fnc with test_mode=0.  See cmd_fnc (above) for details.
149
150    Note the "u" in "cmd_fnc_u" stands for "unconditional".
151    """
152
153    return cmd_fnc(
154        cmd_buf,
155        test_mode=0,
156        quiet=quiet,
157        debug=debug,
158        print_output=print_output,
159        show_err=show_err,
160        return_stderr=return_stderr,
161        ignore_err=ignore_err,
162    )
163
164
165def parse_command_string(command_string):
166    r"""
167    Parse a bash command-line command string and return the result as a dictionary of parms.
168
169    This can be useful for answering questions like "What did the user specify as the value for parm x in the
170    command string?".
171
172    This function expects the command string to follow the following posix conventions:
173    - Short parameters:
174      -<parm name><space><arg value>
175    - Long parameters:
176      --<parm name>=<arg value>
177
178    The first item in the string will be considered to be the command.  All values not conforming to the
179    specifications above will be considered positional parms.  If there are multiple parms with the same
180    name, they will be put into a list (see illustration below where "-v" is specified multiple times).
181
182    Description of argument(s):
183    command_string                  The complete command string including all parameters and arguments.
184
185    Sample input:
186
187    robot_cmd_buf:                                    robot -v OPENBMC_HOST:dummy1 -v keyword_string:'Set
188    Auto Reboot  no' -v lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot -v quiet:0 -v
189    test_mode:0 -v debug:0 --outputdir='/home/user1/status/children/'
190    --output=dummy1.Auto_reboot.170802.124544.output.xml --log=dummy1.Auto_reboot.170802.124544.log.html
191    --report=dummy1.Auto_reboot.170802.124544.report.html
192    /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
193
194    Sample output:
195
196    robot_cmd_buf_dict:
197      robot_cmd_buf_dict[command]:                    robot
198      robot_cmd_buf_dict[v]:
199        robot_cmd_buf_dict[v][0]:                     OPENBMC_HOST:dummy1
200        robot_cmd_buf_dict[v][1]:                     keyword_string:Set Auto Reboot no
201        robot_cmd_buf_dict[v][2]:
202        lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot
203        robot_cmd_buf_dict[v][3]:                     quiet:0
204        robot_cmd_buf_dict[v][4]:                     test_mode:0
205        robot_cmd_buf_dict[v][5]:                     debug:0
206      robot_cmd_buf_dict[outputdir]:                  /home/user1/status/children/
207      robot_cmd_buf_dict[output]:                     dummy1.Auto_reboot.170802.124544.output.xml
208      robot_cmd_buf_dict[log]:                        dummy1.Auto_reboot.170802.124544.log.html
209      robot_cmd_buf_dict[report]:                     dummy1.Auto_reboot.170802.124544.report.html
210      robot_cmd_buf_dict[positional]:
211      /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
212    """
213
214    # We want the parms in the string broken down the way bash would do it, so we'll call upon bash to do
215    # that by creating a simple inline bash function.
216    bash_func_def = (
217        'function parse { for parm in "${@}" ; do' + " echo $parm ; done ; }"
218    )
219
220    rc, outbuf = cmd_fnc_u(
221        bash_func_def + " ; parse " + command_string, quiet=1, print_output=0
222    )
223    command_string_list = outbuf.rstrip("\n").split("\n")
224
225    command_string_dict = collections.OrderedDict()
226    ix = 1
227    command_string_dict["command"] = command_string_list[0]
228    while ix < len(command_string_list):
229        if command_string_list[ix].startswith("--"):
230            key, value = command_string_list[ix].split("=")
231            key = key.lstrip("-")
232        elif command_string_list[ix].startswith("-"):
233            key = command_string_list[ix].lstrip("-")
234            ix += 1
235            try:
236                value = command_string_list[ix]
237            except IndexError:
238                value = ""
239        else:
240            key = "positional"
241            value = command_string_list[ix]
242        if key in command_string_dict:
243            if isinstance(command_string_dict[key], str):
244                command_string_dict[key] = [command_string_dict[key]]
245            command_string_dict[key].append(value)
246        else:
247            command_string_dict[key] = value
248        ix += 1
249
250    return command_string_dict
251
252
253# Save the original SIGALRM handler for later restoration by shell_cmd.
254original_sigalrm_handler = signal.getsignal(signal.SIGALRM)
255
256
257def shell_cmd_timed_out(signal_number, frame):
258    r"""
259    Handle an alarm signal generated during the shell_cmd function.
260    """
261
262    gp.dprint_executing()
263    global command_timed_out
264    command_timed_out = True
265    # Get subprocess pid from shell_cmd's call stack.
266    sub_proc = gp.get_stack_var("sub_proc", 0)
267    pid = sub_proc.pid
268    gp.dprint_var(pid)
269    # Terminate the child process group.
270    os.killpg(pid, signal.SIGKILL)
271    # Restore the original SIGALRM handler.
272    signal.signal(signal.SIGALRM, original_sigalrm_handler)
273
274    return
275
276
277def shell_cmd(
278    command_string,
279    quiet=None,
280    print_output=None,
281    show_err=1,
282    test_mode=0,
283    time_out=None,
284    max_attempts=1,
285    retry_sleep_time=5,
286    valid_rcs=[0],
287    ignore_err=None,
288    return_stderr=0,
289    fork=0,
290    error_regexes=None,
291):
292    r"""
293    Run the given command string in a shell and return a tuple consisting of the shell return code and the
294    output.
295
296    Description of argument(s):
297    command_string                  The command string to be run in a shell (e.g. "ls /tmp").
298    quiet                           If set to 0, this function will print "Issuing: <cmd string>" to stdout.
299                                    When the quiet argument is set to None, this function will assign a
300                                    default value by searching upward in the stack for the quiet variable
301                                    value.  If no such value is found, quiet is set to 0.
302    print_output                    If this is set, this function will print the stdout/stderr generated by
303                                    the shell command to stdout.
304    show_err                        If show_err is set, this function will print a standardized error report
305                                    if the shell command fails (i.e. if the shell command returns a shell_rc
306                                    that is not in valid_rcs).  Note: Error text is only printed if ALL
307                                    attempts to run the command_string fail.  In other words, if the command
308                                    execution is ultimately successful, initial failures are hidden.
309    test_mode                       If test_mode is set, this function will not actually run the command.  If
310                                    print_output is also set, this function will print "(test_mode) Issuing:
311                                    <cmd string>" to stdout.  A caller should call shell_cmd directly if they
312                                    wish to have the command string run unconditionally.  They should call
313                                    the t_shell_cmd wrapper (defined below) if they wish to run the command
314                                    string only if the prevailing test_mode variable is set to 0.
315    time_out                        A time-out value expressed in seconds.  If the command string has not
316                                    finished executing within <time_out> seconds, it will be halted and
317                                    counted as an error.
318    max_attempts                    The max number of attempts that should be made to run the command string.
319    retry_sleep_time                The number of seconds to sleep between attempts.
320    valid_rcs                       A list of integers indicating which shell_rc values are not to be
321                                    considered errors.
322    ignore_err                      Ignore error means that a failure encountered by running the command
323                                    string will not be raised as a python exception.  When the ignore_err
324                                    argument is set to None, this function will assign a default value by
325                                    searching upward in the stack for the ignore_err variable value.  If no
326                                    such value is found, ignore_err is set to 1.
327    return_stderr                   If return_stderr is set, this function will process the stdout and stderr
328                                    streams from the shell command separately.  In such a case, the tuple
329                                    returned by this function will consist of three values rather than just
330                                    two: rc, stdout, stderr.
331    fork                            Run the command string asynchronously (i.e. don't wait for status of the
332                                    child process and don't try to get stdout/stderr) and return the Popen
333                                    object created by the subprocess.popen() function.  See the kill_cmd
334                                    function for details on how to process the popen object.
335    error_regexes                   A list of regular expressions to be used to identify errors in the
336                                    command output.  If there is a match for any of these regular
337                                    expressions, the command will be considered a failure and the shell_rc
338                                    will be set to -1.  For example, if error_regexes = ['ERROR:'] and the
339                                    command output contains 'ERROR:  Unrecognized option', it will be counted
340                                    as an error even if the command returned 0.  This is useful when running
341                                    commands that do not always return non-zero on error.
342    """
343
344    err_msg = gv.valid_value(command_string)
345    if err_msg:
346        raise ValueError(err_msg)
347
348    # Assign default values to some of the arguments to this function.
349    quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0)))
350    print_output = int(gm.dft(print_output, not quiet))
351    show_err = int(show_err)
352    ignore_err = int(gm.dft(ignore_err, gp.get_stack_var("ignore_err", 1)))
353
354    gp.qprint_issuing(command_string, test_mode)
355    if test_mode:
356        return (0, "", "") if return_stderr else (0, "")
357
358    # Convert a string python dictionary definition to a dictionary.
359    valid_rcs = fa.source_to_object(valid_rcs)
360    # Convert each list entry to a signed value.
361    valid_rcs = [gm.to_signed(x) for x in valid_rcs]
362
363    stderr = subprocess.PIPE if return_stderr else subprocess.STDOUT
364
365    # Write all output to func_out_history_buf rather than directly to stdout.  This allows us to decide
366    # what to print after all attempts to run the command string have been made.  func_out_history_buf will
367    # contain the complete history from the current invocation of this function.
368    global command_timed_out
369    command_timed_out = False
370    func_out_history_buf = ""
371    for attempt_num in range(1, max_attempts + 1):
372        sub_proc = subprocess.Popen(
373            command_string,
374            bufsize=1,
375            shell=True,
376            universal_newlines=True,
377            executable="/bin/bash",
378            stdin=subprocess.PIPE,
379            stdout=subprocess.PIPE,
380            stderr=stderr,
381        )
382        if fork:
383            return sub_proc
384
385        if time_out:
386            command_timed_out = False
387            # Designate a SIGALRM handling function and set alarm.
388            signal.signal(signal.SIGALRM, shell_cmd_timed_out)
389            signal.alarm(time_out)
390        try:
391            stdout_buf, stderr_buf = sub_proc.communicate()
392        except IOError:
393            command_timed_out = True
394        # Restore the original SIGALRM handler and clear the alarm.
395        signal.signal(signal.SIGALRM, original_sigalrm_handler)
396        signal.alarm(0)
397
398        # Output from this loop iteration is written to func_out_buf for later processing.  This can include
399        # stdout, stderr and our own error messages.
400        func_out_buf = ""
401        if print_output:
402            if return_stderr:
403                func_out_buf += stderr_buf
404            func_out_buf += stdout_buf
405        shell_rc = sub_proc.returncode
406        if shell_rc in valid_rcs:
407            # Check output for text indicating there is an error.
408            if error_regexes and re.match("|".join(error_regexes), stdout_buf):
409                shell_rc = -1
410            else:
411                break
412        err_msg = "The prior shell command failed.\n"
413        err_msg += gp.sprint_var(attempt_num)
414        err_msg += gp.sprint_vars(command_string, command_timed_out, time_out)
415        err_msg += gp.sprint_varx("child_pid", sub_proc.pid)
416        err_msg += gp.sprint_vars(shell_rc, valid_rcs, fmt=gp.hexa())
417        if error_regexes:
418            err_msg += gp.sprint_vars(error_regexes)
419        if not print_output:
420            if return_stderr:
421                err_msg += "stderr_buf:\n" + stderr_buf
422            err_msg += "stdout_buf:\n" + stdout_buf
423        if show_err:
424            func_out_buf += gp.sprint_error_report(err_msg)
425        if attempt_num < max_attempts:
426            cmd_buf = "time.sleep(" + str(retry_sleep_time) + ")"
427            if show_err:
428                func_out_buf += gp.sprint_issuing(cmd_buf)
429            exec(cmd_buf)
430        func_out_history_buf += func_out_buf
431
432    if shell_rc in valid_rcs:
433        gp.gp_print(func_out_buf)
434    else:
435        if show_err:
436            gp.gp_print(func_out_history_buf, stream="stderr")
437        else:
438            # There is no error information to show so just print output from last loop iteration.
439            gp.gp_print(func_out_buf)
440        if not ignore_err:
441            # If the caller has already asked to show error info, avoid repeating that in the failure message.
442            err_msg = (
443                "The prior shell command failed.\n" if show_err else err_msg
444            )
445            if robot_env:
446                BuiltIn().fail(err_msg)
447            else:
448                raise ValueError(err_msg)
449
450    return (
451        (shell_rc, stdout_buf, stderr_buf)
452        if return_stderr
453        else (shell_rc, stdout_buf)
454    )
455
456
457def t_shell_cmd(command_string, **kwargs):
458    r"""
459    Search upward in the the call stack to obtain the test_mode argument, add it to kwargs and then call
460    shell_cmd and return the result.
461
462    See shell_cmd prolog for details on all arguments.
463    """
464
465    if "test_mode" in kwargs:
466        error_message = (
467            "Programmer error - test_mode is not a valid"
468            + " argument to this function."
469        )
470        gp.print_error_report(error_message)
471        exit(1)
472
473    test_mode = int(gp.get_stack_var("test_mode", 0))
474    kwargs["test_mode"] = test_mode
475
476    return shell_cmd(command_string, **kwargs)
477
478
479def kill_cmd(popen, sig=signal.SIGTERM):
480    r"""
481    Kill the subprocess represented by the Popen object and return a tuple consisting of the shell return
482    code and the output.
483
484    This function is meant to be used as the follow-up for a call to shell_cmd(..., fork=1).
485
486    Example:
487    popen = shell_cmd("some_pgm.py", fork=1)
488    ...
489    shell_rc, output = kill_cmd(popen)
490
491    Description of argument(s):
492    popen                           A Popen object returned by the subprocess.Popen() command.
493    sig                             The signal to be sent to the child process.
494    """
495
496    gp.dprint_var(popen.pid)
497    os.killpg(popen.pid, sig)
498    stdout, stderr = popen.communicate()
499    shell_rc = popen.returncode
500    return (shell_rc, stdout, stderr) if stderr else (shell_rc, stdout)
501
502
503def re_order_kwargs(stack_frame_ix, **kwargs):
504    r"""
505    Re-order the kwargs to match the order in which they were specified on a function invocation and return
506    as an ordered dictionary.
507
508    Note that this re_order_kwargs function should not be necessary in python versions 3.6 and beyond.
509
510    Example:
511
512    The caller calls func1 like this:
513
514    func1('mike', arg1='one', arg2='two', arg3='three')
515
516    And func1 is defined as follows:
517
518    def func1(first_arg, **kwargs):
519
520        kwargs = re_order_kwargs(first_arg_num=2, stack_frame_ix=3, **kwargs)
521
522    The kwargs dictionary before calling re_order_kwargs (where order is not guaranteed):
523
524    kwargs:
525      kwargs[arg3]:          three
526      kwargs[arg2]:          two
527      kwargs[arg1]:          one
528
529    The kwargs dictionary after calling re_order_kwargs:
530
531    kwargs:
532      kwargs[arg1]:          one
533      kwargs[arg2]:          two
534      kwargs[arg3]:          three
535
536    Note that the re-ordered kwargs match the order specified on the call to func1.
537
538    Description of argument(s):
539    stack_frame_ix                  The stack frame of the function whose kwargs values must be re-ordered.
540                                    0 is the stack frame of re_order_kwargs, 1 is the stack from of its
541                                    caller and so on.
542    kwargs                          The keyword argument dictionary which is to be re-ordered.
543    """
544
545    new_kwargs = collections.OrderedDict()
546
547    # Get position number of first keyword on the calling line of code.
548    (args, varargs, keywords, locals) = inspect.getargvalues(
549        inspect.stack()[stack_frame_ix][0]
550    )
551    first_kwarg_pos = 1 + len(args)
552    if varargs is not None:
553        first_kwarg_pos += len(locals[varargs])
554    for arg_num in range(first_kwarg_pos, first_kwarg_pos + len(kwargs)):
555        # This will result in an arg_name value such as "arg1='one'".
556        arg_name = gp.get_arg_name(None, arg_num, stack_frame_ix + 2)
557        # Continuing with the prior example, the following line will result
558        # in key being set to 'arg1'.
559        key = arg_name.split("=")[0]
560        new_kwargs[key] = kwargs[key]
561
562    return new_kwargs
563
564
565def default_arg_delim(arg_dashes):
566    r"""
567    Return the default argument delimiter value for the given arg_dashes value.
568
569    Note: this function is useful for functions that manipulate bash command line arguments (e.g. --parm=1 or
570    -parm 1).
571
572    Description of argument(s):
573    arg_dashes                      The argument dashes specifier (usually, "-" or "--").
574    """
575
576    if arg_dashes == "--":
577        return "="
578
579    return " "
580
581
582def create_command_string(command, *pos_parms, **options):
583    r"""
584    Create and return a bash command string consisting of the given arguments formatted as text.
585
586    The default formatting of options is as follows:
587
588    <single dash><option name><space delim><option value>
589
590    Example:
591
592    -parm value
593
594    The caller can change the kind of dashes/delimiters used by specifying "arg_dashes" and/or "arg_delims"
595    as options.  These options are processed specially by the create_command_string function and do NOT get
596    inserted into the resulting command string.  All options following the arg_dashes/arg_delims options will
597    then use the specified values for dashes/delims.  In the special case of arg_dashes equal to "--", the
598    arg_delim will automatically be changed to "=".  See examples below.
599
600    Quoting rules:
601
602    The create_command_string function will single quote option values as needed to prevent bash expansion.
603    If the caller wishes to defeat this action, they may single or double quote the option value themselves.
604    See examples below.
605
606    pos_parms are NOT automatically quoted.  The caller is advised to either explicitly add quotes or to use
607    the quote_bash_parm functions to quote any pos_parms.
608
609    Examples:
610
611    command_string = create_command_string('cd', '~')
612
613    Result:
614    cd ~
615
616    Note that the pos_parm ("~") does NOT get quoted, as per the aforementioned rules.  If quotes are
617    desired, they may be added explicitly by the caller:
618
619    command_string = create_command_string('cd', '\'~\'')
620
621    Result:
622    cd '~'
623
624    command_string = create_command_string('grep', '\'^[^ ]*=\'',
625        '/tmp/myfile', i=None, m='1', arg_dashes='--', color='always')
626
627    Result:
628    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
629
630    In the preceding example, note the use of None to cause the "i" parm to be treated as a flag (i.e. no
631    argument value is generated).  Also, note the use of arg_dashes to change the type of dashes used on all
632    subsequent options.  The following example is equivalent to the prior.  Note that quote_bash_parm is used
633    instead of including the quotes explicitly.
634
635    command_string = create_command_string('grep', quote_bash_parm('^[^ ]*='),
636        '/tmp/myfile', i=None,  m='1', arg_dashes='--', color='always')
637
638    Result:
639    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
640
641    In the following example, note the automatic quoting of the password option, as per the aforementioned
642    rules.
643
644    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
645        m='1', arg_dashes='--', password='${my_pw}')
646
647    However, let's say that the caller wishes to have bash expand the password value.  To achieve this, the
648    caller can use double quotes:
649
650    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
651        m='1', arg_dashes='--', password='"${my_pw}"')
652
653    Result:
654    my_pgm -i -m 1 --password="${my_pw}" /tmp/myfile
655
656    command_string = create_command_string('ipmitool', 'power status',
657        I='lanplus', C='3', 'p=623', U='root', P='0penBmc', H='xx.xx.xx.xx')
658
659    Result:
660    ipmitool -I lanplus -C 3 -p 623 -U root -P 0penBmc -H xx.xx.xx.xx power status
661
662    By default create_command_string will take measures to preserve the order of the callers options.  In
663    some cases, this effort may fail (as when calling directly from a robot program).  In this case, the
664    caller can accept the responsibility of keeping an ordered list of options by calling this function with
665    the last positional parm as some kind of dictionary (preferably an OrderedDict) and avoiding the use of
666    any actual option args.
667
668    Example:
669    kwargs = collections.OrderedDict([('pass', 0), ('fail', 0)])
670    command_string = create_command_string('my program', 'pos_parm1', kwargs)
671
672    Result:
673
674    my program -pass 0 -fail 0 pos_parm1
675
676    Note to programmers who wish to write a wrapper to this function:  If the python version is less than
677    3.6, to get the options to be processed correctly, the wrapper function must include a _stack_frame_ix_
678    keyword argument to allow this function to properly re-order options:
679
680    def create_ipmi_ext_command_string(command, **kwargs):
681
682        return create_command_string('ipmitool', command, _stack_frame_ix_=2,
683            **kwargs)
684
685    Example call of wrapper function:
686
687    command_string = create_ipmi_ext_command_string('power status', I='lanplus')
688
689    Description of argument(s):
690    command                         The command (e.g. "cat", "sort", "ipmitool", etc.).
691    pos_parms                       The positional parms for the command (e.g. PATTERN, FILENAME, etc.).
692                                    These will be placed at the end of the resulting command string.
693    options                         The command options (e.g. "-m 1", "--max-count=NUM", etc.).  Note that if
694                                    the value of any option is None, then it will be understood to be a flag
695                                    (for which no value is required).
696    """
697
698    arg_dashes = "-"
699    delim = default_arg_delim(arg_dashes)
700
701    command_string = command
702
703    if len(pos_parms) > 0 and gp.is_dict(pos_parms[-1]):
704        # Convert pos_parms from tuple to list.
705        pos_parms = list(pos_parms)
706        # Re-assign options to be the last pos_parm value (which is a dictionary).
707        options = pos_parms[-1]
708        # Now delete the last pos_parm.
709        del pos_parms[-1]
710    else:
711        # Either get stack_frame_ix from the caller via options or set it to the default value.
712        stack_frame_ix = options.pop("_stack_frame_ix_", 1)
713        if gm.python_version < gm.ordered_dict_version:
714            # Re-establish the original options order as specified on the original line of code.  This
715            # function depends on correct order.
716            options = re_order_kwargs(stack_frame_ix, **options)
717    for key, value in options.items():
718        # Check for special values in options and process them.
719        if key == "arg_dashes":
720            arg_dashes = str(value)
721            delim = default_arg_delim(arg_dashes)
722            continue
723        if key == "arg_delim":
724            delim = str(value)
725            continue
726        # Format the options elements into the command string.
727        command_string += " " + arg_dashes + key
728        if value is not None:
729            command_string += delim
730            if re.match(r'^(["].*["]|[\'].*[\'])$', str(value)):
731                # Already quoted.
732                command_string += str(value)
733            else:
734                command_string += gm.quote_bash_parm(str(value))
735    # Finally, append the pos_parms to the end of the command_string.  Use filter to eliminate blank pos
736    # parms.
737    command_string = " ".join([command_string] + list(filter(None, pos_parms)))
738
739    return command_string
740