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    # Get subprocess pid from shell_cmd's call stack.
272    sub_proc = gp.get_stack_var('sub_proc', 0)
273    pid = sub_proc.pid
274    # Terminate the child process.
275    os.kill(pid, signal.SIGTERM)
276    # Restore the original SIGALRM handler.
277    signal.signal(signal.SIGALRM, original_sigalrm_handler)
278
279    return
280
281
282def shell_cmd(command_string,
283              quiet=None,
284              print_output=None,
285              show_err=1,
286              test_mode=0,
287              time_out=None,
288              max_attempts=1,
289              retry_sleep_time=5,
290              allowed_shell_rcs=[0],
291              ignore_err=None,
292              return_stderr=0,
293              fork=0):
294    r"""
295    Run the given command string in a shell and return a tuple consisting of
296    the shell return code and the output.
297
298    Description of argument(s):
299    command_string                  The command string to be run in a shell
300                                    (e.g. "ls /tmp").
301    quiet                           If set to 0, this function will print
302                                    "Issuing: <cmd string>" to stdout.  When
303                                    the quiet argument is set to None, this
304                                    function will assign a default value by
305                                    searching upward in the stack for the
306                                    quiet variable value.  If no such value is
307                                    found, quiet is set to 0.
308    print_output                    If this is set, this function will print
309                                    the stdout/stderr generated by the shell
310                                    command to stdout.
311    show_err                        If show_err is set, this function will
312                                    print a standardized error report if the
313                                    shell command fails (i.e. if the shell
314                                    command returns a shell_rc that is not in
315                                    allowed_shell_rcs).  Note: Error text is
316                                    only printed if ALL attempts to run the
317                                    command_string fail.  In other words, if
318                                    the command execution is ultimately
319                                    successful, initial failures are hidden.
320    test_mode                       If test_mode is set, this function will
321                                    not actually run the command.  If
322                                    print_output is also set, this function
323                                    will print "(test_mode) Issuing: <cmd
324                                    string>" to stdout.  A caller should call
325                                    shell_cmd directly if they wish to have
326                                    the command string run unconditionally.
327                                    They should call the t_shell_cmd wrapper
328                                    (defined below) if they wish to run the
329                                    command string only if the prevailing
330                                    test_mode variable is set to 0.
331    time_out                        A time-out value expressed in seconds.  If
332                                    the command string has not finished
333                                    executing within <time_out> seconds, it
334                                    will be halted and counted as an error.
335    max_attempts                    The max number of attempts that should be
336                                    made to run the command string.
337    retry_sleep_time                The number of seconds to sleep between
338                                    attempts.
339    allowed_shell_rcs               A list of integers indicating which
340                                    shell_rc values are not to be considered
341                                    errors.
342    ignore_err                      Ignore error means that a failure
343                                    encountered by running the command string
344                                    will not be raised as a python exception.
345                                    When the ignore_err argument is set to
346                                    None, this function will assign a default
347                                    value by searching upward in the stack for
348                                    the ignore_err variable value.  If no such
349                                    value is found, ignore_err is set to 1.
350    return_stderr                   If return_stderr is set, this function
351                                    will process the stdout and stderr streams
352                                    from the shell command separately.  In
353                                    such a case, the tuple returned by this
354                                    function will consist of three values
355                                    rather than just two: rc, stdout, stderr.
356    fork                            Run the command string asynchronously
357                                    (i.e. don't wait for status of the child
358                                    process and don't try to get
359                                    stdout/stderr).
360    """
361
362    # Assign default values to some of the arguments to this function.
363    quiet = int(gm.dft(quiet, gp.get_stack_var('quiet', 0)))
364    print_output = int(gm.dft(print_output, not quiet))
365    show_err = int(show_err)
366    global_ignore_err = gp.get_var_value(ignore_err, 1)
367    stack_ignore_err = gp.get_stack_var('ignore_err', global_ignore_err)
368    ignore_err = int(gm.dft(ignore_err, gm.dft(stack_ignore_err, 1)))
369
370    err_msg = gv.valid_value(command_string)
371    if err_msg != "":
372        raise ValueError(err_msg)
373
374    if not quiet:
375        gp.print_issuing(command_string, test_mode)
376
377    if test_mode:
378        if return_stderr:
379            return 0, "", ""
380        else:
381            return 0, ""
382
383    # Convert each list entry to a signed value.
384    allowed_shell_rcs = fa.source_to_object(allowed_shell_rcs)
385    allowed_shell_rcs = [gm.to_signed(x) for x in allowed_shell_rcs]
386
387    if return_stderr:
388        stderr = subprocess.PIPE
389    else:
390        stderr = subprocess.STDOUT
391
392    shell_rc = 0
393    out_buf = ""
394    err_buf = ""
395    # Write all output to func_history_stdout rather than directly to stdout.
396    # This allows us to decide what to print after all attempts to run the
397    # command string have been made.  func_history_stdout will contain the
398    # complete stdout history from the current invocation of this function.
399    func_history_stdout = ""
400    for attempt_num in range(1, max_attempts + 1):
401        sub_proc = subprocess.Popen(command_string,
402                                    bufsize=1,
403                                    shell=True,
404                                    executable='/bin/bash',
405                                    stdout=subprocess.PIPE,
406                                    stderr=stderr)
407        out_buf = ""
408        err_buf = ""
409        # Output from this loop iteration is written to func_stdout for later
410        # processing.
411        func_stdout = ""
412        if fork:
413            break
414        command_timed_out = False
415        if time_out is not None:
416            # Designate a SIGALRM handling function and set alarm.
417            signal.signal(signal.SIGALRM, shell_cmd_timed_out)
418            signal.alarm(time_out)
419        try:
420            if return_stderr:
421                for line in sub_proc.stderr:
422                    try:
423                        err_buf += line
424                    except TypeError:
425                        line = line.decode("utf-8")
426                        err_buf += line
427                    if not print_output:
428                        continue
429                    func_stdout += line
430            for line in sub_proc.stdout:
431                try:
432                    out_buf += line
433                except TypeError:
434                    line = line.decode("utf-8")
435                    out_buf += line
436                if not print_output:
437                    continue
438                func_stdout += line
439        except IOError:
440            command_timed_out = True
441        sub_proc.communicate()
442        shell_rc = sub_proc.returncode
443        # Restore the original SIGALRM handler and clear the alarm.
444        signal.signal(signal.SIGALRM, original_sigalrm_handler)
445        signal.alarm(0)
446        if shell_rc in allowed_shell_rcs:
447            break
448        err_msg = "The prior shell command failed.\n"
449        if quiet:
450            err_msg += gp.sprint_var(command_string)
451        if command_timed_out:
452            err_msg += gp.sprint_var(command_timed_out)
453            err_msg += gp.sprint_var(time_out)
454            err_msg += gp.sprint_varx("child_pid", sub_proc.pid)
455        err_msg += gp.sprint_var(attempt_num)
456        err_msg += gp.sprint_var(shell_rc, gp.hexa())
457        err_msg += gp.sprint_var(allowed_shell_rcs, gp.hexa())
458        if not print_output:
459            if return_stderr:
460                err_msg += "err_buf:\n" + err_buf
461            err_msg += "out_buf:\n" + out_buf
462        if show_err:
463            func_stdout += gp.sprint_error_report(err_msg)
464        func_history_stdout += func_stdout
465        if attempt_num < max_attempts:
466            func_history_stdout += gp.sprint_issuing("time.sleep("
467                                                     + str(retry_sleep_time)
468                                                     + ")")
469            time.sleep(retry_sleep_time)
470
471    if shell_rc not in allowed_shell_rcs:
472        func_stdout = func_history_stdout
473
474    gp.gp_print(func_stdout)
475
476    if shell_rc not in allowed_shell_rcs:
477        if not ignore_err:
478            if robot_env:
479                BuiltIn().fail(err_msg)
480            else:
481                raise ValueError("The prior shell command failed.\n")
482
483    if return_stderr:
484        return shell_rc, out_buf, err_buf
485    else:
486        return shell_rc, out_buf
487
488
489def t_shell_cmd(command_string, **kwargs):
490    r"""
491    Search upward in the the call stack to obtain the test_mode argument, add
492    it to kwargs and then call shell_cmd and return the result.
493
494    See shell_cmd prolog for details on all arguments.
495    """
496
497    if 'test_mode' in kwargs:
498        error_message = "Programmer error - test_mode is not a valid" +\
499            " argument to this function."
500        gp.print_error_report(error_message)
501        exit(1)
502
503    test_mode = gp.get_stack_var('test_mode',
504                                 int(gp.get_var_value(None, 0, "test_mode")))
505    kwargs['test_mode'] = test_mode
506
507    return shell_cmd(command_string, **kwargs)
508
509
510def re_order_kwargs(stack_frame_ix, **kwargs):
511    r"""
512    Re-order the kwargs to match the order in which they were specified on a
513    function invocation and return as an ordered dictionary.
514
515    Note that this re_order_kwargs function should not be necessary in python
516    versions 3.6 and beyond.
517
518    Example:
519
520    The caller calls func1 like this:
521
522    func1('mike', arg1='one', arg2='two', arg3='three')
523
524    And func1 is defined as follows:
525
526    def func1(first_arg, **kwargs):
527
528        kwargs = re_order_kwargs(first_arg_num=2, stack_frame_ix=3, **kwargs)
529
530    The kwargs dictionary before calling re_order_kwargs (where order is not
531    guaranteed):
532
533    kwargs:
534      kwargs[arg3]:          three
535      kwargs[arg2]:          two
536      kwargs[arg1]:          one
537
538    The kwargs dictionary after calling re_order_kwargs:
539
540    kwargs:
541      kwargs[arg1]:          one
542      kwargs[arg2]:          two
543      kwargs[arg3]:          three
544
545    Note that the re-ordered kwargs match the order specified on the call to
546    func1.
547
548    Description of argument(s):
549    stack_frame_ix                  The stack frame of the function whose
550                                    kwargs values must be re-ordered.  0 is
551                                    the stack frame of re_order_kwargs, 1 is
552                                    the stack from of its caller and so on.
553    kwargs                          The keyword argument dictionary which is
554                                    to be re-ordered.
555    """
556
557    new_kwargs = collections.OrderedDict()
558
559    # Get position number of first keyword on the calling line of code.
560    (args, varargs, keywords, locals) =\
561        inspect.getargvalues(inspect.stack()[stack_frame_ix][0])
562    first_kwarg_pos = 1 + len(args)
563    if varargs is not None:
564        first_kwarg_pos += len(locals[varargs])
565    for arg_num in range(first_kwarg_pos, first_kwarg_pos + len(kwargs)):
566        # This will result in an arg_name value such as "arg1='one'".
567        arg_name = gp.get_arg_name(None, arg_num, stack_frame_ix + 2)
568        # Continuing with the prior example, the following line will result
569        # in key being set to 'arg1'.
570        key = arg_name.split('=')[0]
571        new_kwargs[key] = kwargs[key]
572
573    return new_kwargs
574
575
576def default_arg_delim(arg_dashes):
577    r"""
578    Return the default argument delimiter value for the given arg_dashes value.
579
580    Note: this function is useful for functions that manipulate bash command
581    line arguments (e.g. --parm=1 or -parm 1).
582
583    Description of argument(s):
584    arg_dashes                      The argument dashes specifier (usually,
585                                    "-" or "--").
586    """
587
588    if arg_dashes == "--":
589        return "="
590
591    return " "
592
593
594def create_command_string(command, *pos_parms, **options):
595    r"""
596    Create and return a bash command string consisting of the given arguments
597    formatted as text.
598
599    The default formatting of options is as follows:
600
601    <single dash><option name><space delim><option value>
602
603    Example:
604
605    -parm value
606
607    The caller can change the kind of dashes/delimiters used by specifying
608    "arg_dashes" and/or "arg_delims" as options.  These options are processed
609    specially by the create_command_string function and do NOT get inserted
610    into the resulting command string.  All options following the
611    arg_dashes/arg_delims options will then use the specified values for
612    dashes/delims.  In the special case of arg_dashes equal to "--", the
613    arg_delim will automatically be changed to "=".  See examples below.
614
615    Quoting rules:
616
617    The create_command_string function will single quote option values as
618    needed to prevent bash expansion.  If the caller wishes to defeat this
619    action, they may single or double quote the option value themselves.  See
620    examples below.
621
622    pos_parms are NOT automatically quoted.  The caller is advised to either
623    explicitly add quotes or to use the quote_bash_parm functions to quote any
624    pos_parms.
625
626    Examples:
627
628    command_string = create_command_string('cd', '~')
629
630    Result:
631    cd ~
632
633    Note that the pos_parm ("~") does NOT get quoted, as per the
634    aforementioned rules.  If quotes are desired, they may be added explicitly
635    by the caller:
636
637    command_string = create_command_string('cd', '\'~\'')
638
639    Result:
640    cd '~'
641
642    command_string = create_command_string('grep', '\'^[^ ]*=\'',
643        '/tmp/myfile', i=None, m='1', arg_dashes='--', color='always')
644
645    Result:
646    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
647
648    In the preceding example, note the use of None to cause the "i" parm to be
649    treated as a flag (i.e. no argument value is generated).  Also, note the
650    use of arg_dashes to change the type of dashes used on all subsequent
651    options.  The following example is equivalent to the prior.  Note that
652    quote_bash_parm is used instead of including the quotes explicitly.
653
654    command_string = create_command_string('grep', quote_bash_parm('^[^ ]*='),
655        '/tmp/myfile', i=None,  m='1', arg_dashes='--', color='always')
656
657    Result:
658    grep -i -m 1 --color=always '^[^ ]*=' /tmp/myfile
659
660    In the following example, note the automatic quoting of the password
661    option, as per the aforementioned rules.
662
663    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
664        m='1', arg_dashes='--', password='${my_pw}')
665
666    However, let's say that the caller wishes to have bash expand the password
667    value.  To achieve this, the caller can use double quotes:
668
669    command_string = create_command_string('my_pgm', '/tmp/myfile', i=None,
670        m='1', arg_dashes='--', password='"${my_pw}"')
671
672    Result:
673    my_pgm -i -m 1 --password="${my_pw}" /tmp/myfile
674
675    command_string = create_command_string('ipmitool', 'power status',
676        I='lanplus', C='3', U='root', P='0penBmc', H='wsbmc010')
677
678    Result:
679    ipmitool -I lanplus -C 3 -U root -P 0penBmc -H wsbmc010 power status
680
681    By default create_command_string will take measures to preserve the order
682    of the callers options.  In some cases, this effort may fail (as when
683    calling directly from a robot program).  In this case, the caller can
684    accept the responsibility of keeping an ordered list of options by calling
685    this function with the last positional parm as some kind of dictionary
686    (preferably an OrderedDict) and avoiding the use of any actual option args.
687
688    Example:
689    kwargs = collections.OrderedDict([('pass', 0), ('fail', 0)])
690    command_string = create_command_string('my program', 'pos_parm1', kwargs)
691
692    Result:
693
694    my program -pass 0 -fail 0 pos_parm1
695
696    Note to programmers who wish to write a wrapper to this function:  To get
697    the options to be processed correctly, the wrapper function must include a
698    _stack_frame_ix_ keyword argument to allow this function to properly
699    re-order options:
700
701    def create_ipmi_ext_command_string(command, **kwargs):
702
703        return create_command_string('ipmitool', command, _stack_frame_ix_=2,
704            **kwargs)
705
706    Example call of wrapper function:
707
708    command_string = create_ipmi_ext_command_string('power status',
709    I='lanplus')
710
711    Description of argument(s):
712    command                         The command (e.g. "cat", "sort",
713                                    "ipmitool", etc.).
714    pos_parms                       The positional parms for the command (e.g.
715                                    PATTERN, FILENAME, etc.).  These will be
716                                    placed at the end of the resulting command
717                                    string.
718    options                         The command options (e.g. "-m 1",
719                                    "--max-count=NUM", etc.).  Note that if
720                                    the value of any option is None, then it
721                                    will be understood to be a flag (for which
722                                    no value is required).
723    """
724
725    arg_dashes = "-"
726    delim = default_arg_delim(arg_dashes)
727
728    command_string = command
729
730    if len(pos_parms) > 0 and gp.is_dict(pos_parms[-1]):
731        # Convert pos_parms from tuple to list.
732        pos_parms = list(pos_parms)
733        # Re-assign options to be the last pos_parm value (which is a
734        # dictionary).
735        options = pos_parms[-1]
736        # Now delete the last pos_parm.
737        del pos_parms[-1]
738    else:
739        # Either get stack_frame_ix from the caller via options or set it to
740        # the default value.
741        if '_stack_frame_ix_' in options:
742            stack_frame_ix = options['_stack_frame_ix_']
743            del options['_stack_frame_ix_']
744        else:
745            stack_frame_ix = 1
746        # Re-establish the original options order as specified on the
747        # original line of code.  This function depends on correct order.
748        options = re_order_kwargs(stack_frame_ix, **options)
749    for key, value in options.items():
750        # Check for special values in options and process them.
751        if key == "arg_dashes":
752            arg_dashes = str(value)
753            delim = default_arg_delim(arg_dashes)
754            continue
755        if key == "arg_delim":
756            delim = str(value)
757            continue
758        # Format the options elements into the command string.
759        command_string += " " + arg_dashes + key
760        if value is not None:
761            command_string += delim
762            if re.match(r'^(["].*["]|[\'].*[\'])$', str(value)):
763                # Already quoted.
764                command_string += str(value)
765            else:
766                command_string += gm.quote_bash_parm(str(value))
767    # Finally, append the pos_parms to the end of the command_string.  Use
768    # filter to eliminate blank pos parms.
769    command_string = ' '.join([command_string] + list(filter(None, pos_parms)))
770
771    return command_string
772