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