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