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