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