blob: 9483bb6a47f817f9ce7859fb6801f55d0f5beb9e [file] [log] [blame]
Michael Walshc3b512e2017-02-20 15:59:01 -06001#!/usr/bin/env python
2
3r"""
4This module provides command execution functions such as cmd_fnc and cmd_fnc_u.
5"""
6
Michael Walsh21083d22018-06-01 14:19:32 -05007import os
Michael Walshc3b512e2017-02-20 15:59:01 -06008import sys
9import subprocess
Michael Walshf41fac82017-08-02 15:05:24 -050010import collections
Michael Walsh21083d22018-06-01 14:19:32 -050011import signal
12import time
George Keishing16244c22019-01-31 16:16:14 +000013import re
14import inspect
Michael Walshc3b512e2017-02-20 15:59:01 -060015
Michael Walshc3b512e2017-02-20 15:59:01 -060016import gen_print as gp
17import gen_valid as gv
18import gen_misc as gm
Michael Walshd6901502017-11-14 12:58:37 -060019
20robot_env = gp.robot_env
21
Michael Walshafc53a22017-04-12 15:52:28 -050022if robot_env:
23 import gen_robot_print as grp
Michael Walsha3e2f532018-01-10 13:43:42 -060024 from robot.libraries.BuiltIn import BuiltIn
Michael Walshc3b512e2017-02-20 15:59:01 -060025
26
Michael Walsh21083d22018-06-01 14:19:32 -050027# cmd_fnc and cmd_fnc_u should now be considered deprecated. shell_cmd and
28# t_shell_cmd should be used instead.
Michael Walshc3b512e2017-02-20 15:59:01 -060029def cmd_fnc(cmd_buf,
30 quiet=None,
31 test_mode=None,
Michael Walshafc53a22017-04-12 15:52:28 -050032 debug=0,
Michael Walshc3b512e2017-02-20 15:59:01 -060033 print_output=1,
Michael Walshcfe9fed2017-09-12 17:13:10 -050034 show_err=1,
Michael Walsha3e2f532018-01-10 13:43:42 -060035 return_stderr=0,
36 ignore_err=1):
Michael Walshc3b512e2017-02-20 15:59:01 -060037 r"""
Michael Walshcfe9fed2017-09-12 17:13:10 -050038 Run the given command in a shell and return the shell return code and the
39 output.
Michael Walshc3b512e2017-02-20 15:59:01 -060040
41 Description of arguments:
42 cmd_buf The command string to be run in a shell.
43 quiet Indicates whether this function should run
Michael Walshcfe9fed2017-09-12 17:13:10 -050044 the print_issuing() function which prints
45 "Issuing: <cmd string>" to stdout.
Michael Walshc3b512e2017-02-20 15:59:01 -060046 test_mode If test_mode is set, this function will
Michael Walshcfe9fed2017-09-12 17:13:10 -050047 not actually run the command. If
48 print_output is set, it will print
49 "(test_mode) Issuing: <cmd string>" to
50 stdout.
Michael Walshc3b512e2017-02-20 15:59:01 -060051 debug If debug is set, this function will print
52 extra debug info.
53 print_output If this is set, this function will print
Michael Walshcfe9fed2017-09-12 17:13:10 -050054 the stdout/stderr generated by the shell
55 command.
Michael Walshc3b512e2017-02-20 15:59:01 -060056 show_err If show_err is set, this function will
Michael Walshcfe9fed2017-09-12 17:13:10 -050057 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.
Michael Walshc3b512e2017-02-20 15:59:01 -060064 """
65
Michael Walshcfe9fed2017-09-12 17:13:10 -050066 # Determine default values.
Michael Walshc3b512e2017-02-20 15:59:01 -060067 quiet = int(gm.global_default(quiet, 0))
68 test_mode = int(gm.global_default(test_mode, 0))
Michael Walshc3b512e2017-02-20 15:59:01 -060069
70 if debug:
Michael Walshafc53a22017-04-12 15:52:28 -050071 gp.print_vars(cmd_buf, quiet, test_mode, debug)
Michael Walshc3b512e2017-02-20 15:59:01 -060072
73 err_msg = gv.svalid_value(cmd_buf)
74 if err_msg != "":
75 raise ValueError(err_msg)
76
77 if not quiet:
Michael Walshafc53a22017-04-12 15:52:28 -050078 gp.pissuing(cmd_buf, test_mode)
Michael Walshc3b512e2017-02-20 15:59:01 -060079
80 if test_mode:
Michael Walshcfe9fed2017-09-12 17:13:10 -050081 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
Michael Walshc3b512e2017-02-20 15:59:01 -060091
92 sub_proc = subprocess.Popen(cmd_buf,
93 bufsize=1,
94 shell=True,
Michael Walsh3ba8ecd2018-04-24 11:33:25 -050095 executable='/bin/bash',
Michael Walshc3b512e2017-02-20 15:59:01 -060096 stdout=subprocess.PIPE,
Michael Walshcfe9fed2017-09-12 17:13:10 -050097 stderr=stderr)
Michael Walshc3b512e2017-02-20 15:59:01 -060098 out_buf = ""
Michael Walshcfe9fed2017-09-12 17:13:10 -050099 if return_stderr:
100 for line in sub_proc.stderr:
George Keishing36efbc02018-12-12 10:18:23 -0600101 try:
102 err_buf += line
103 except TypeError:
104 line = line.decode("utf-8")
105 err_buf += line
Michael Walshcfe9fed2017-09-12 17:13:10 -0500106 if not print_output:
107 continue
108 if robot_env:
109 grp.rprint(line)
110 else:
111 sys.stdout.write(line)
Michael Walshc3b512e2017-02-20 15:59:01 -0600112 for line in sub_proc.stdout:
George Keishing36efbc02018-12-12 10:18:23 -0600113 try:
114 out_buf += line
115 except TypeError:
116 line = line.decode("utf-8")
117 out_buf += line
Michael Walshc3b512e2017-02-20 15:59:01 -0600118 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
Michael Walsha3e2f532018-01-10 13:43:42 -0600128 if shell_rc != 0:
129 err_msg = "The prior shell command failed.\n"
130 err_msg += gp.sprint_var(shell_rc, 1)
Michael Walshcfe9fed2017-09-12 17:13:10 -0500131 if not print_output:
132 err_msg += "out_buf:\n" + out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600133
Michael Walsha3e2f532018-01-10 13:43:42 -0600134 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)
Michael Walshcfe9fed2017-09-12 17:13:10 -0500144
145 if return_stderr:
146 return shell_rc, out_buf, err_buf
147 else:
148 return shell_rc, out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600149
Michael Walshc3b512e2017-02-20 15:59:01 -0600150
Michael Walshc3b512e2017-02-20 15:59:01 -0600151def cmd_fnc_u(cmd_buf,
152 quiet=None,
153 debug=None,
154 print_output=1,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500155 show_err=1,
Michael Walsha3e2f532018-01-10 13:43:42 -0600156 return_stderr=0,
157 ignore_err=1):
Michael Walshc3b512e2017-02-20 15:59:01 -0600158 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,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500165 print_output=print_output, show_err=show_err,
Michael Walsha3e2f532018-01-10 13:43:42 -0600166 return_stderr=return_stderr, ignore_err=ignore_err)
Michael Walshc3b512e2017-02-20 15:59:01 -0600167
Michael Walshf41fac82017-08-02 15:05:24 -0500168
Michael Walshf41fac82017-08-02 15:05:24 -0500169def parse_command_string(command_string):
Michael Walshf41fac82017-08-02 15:05:24 -0500170 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:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500259 if isinstance(command_string_dict[key], str):
Michael Walshf41fac82017-08-02 15:05:24 -0500260 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
Michael Walsh21083d22018-06-01 14:19:32 -0500267
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,
Michael Walsh55abd1b2018-08-31 13:00:50 -0500293 print_output=None,
Michael Walsh21083d22018-06-01 14:19:32 -0500294 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,
Michael Walshfaafa9c2018-06-27 16:39:31 -0500301 return_stderr=0,
302 fork=0):
Michael Walsh21083d22018-06-01 14:19:32 -0500303 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
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500311 "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.
Michael Walsh21083d22018-06-01 14:19:32 -0500317 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.
Michael Walshfaafa9c2018-06-27 16:39:31 -0500365 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).
Michael Walsh21083d22018-06-01 14:19:32 -0500369 """
370
371 # Assign default values to some of the arguments to this function.
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500372 quiet = int(gm.dft(quiet, gp.get_stack_var('quiet', 0)))
Michael Walsh55abd1b2018-08-31 13:00:50 -0500373 print_output = int(gm.dft(print_output, not quiet))
Michael Walsh45fead42018-09-26 17:20:48 -0500374 show_err = int(show_err)
Michael Walsh22c3b6d2018-11-16 15:26:05 -0600375 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)))
Michael Walsh21083d22018-06-01 14:19:32 -0500378
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 = ""
Michael Walshfaafa9c2018-06-27 16:39:31 -0500420 if fork:
421 break
Michael Walsh21083d22018-06-01 14:19:32 -0500422 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:
George Keishing36efbc02018-12-12 10:18:23 -0600430 try:
431 err_buf += line
432 except TypeError:
433 line = line.decode("utf-8")
434 err_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500435 if not print_output:
436 continue
437 func_stdout += line
438 for line in sub_proc.stdout:
George Keishing36efbc02018-12-12 10:18:23 -0600439 try:
440 out_buf += line
441 except TypeError:
442 line = line.decode("utf-8")
443 out_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500444 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"
Michael Walsha750ed72018-07-24 16:18:09 -0500457 if quiet:
458 err_msg += gp.sprint_var(command_string)
Michael Walsh21083d22018-06-01 14:19:32 -0500459 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:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500477 func_history_stdout += gp.sprint_issuing("time.sleep("
478 + str(retry_sleep_time)
479 + ")")
Michael Walsh21083d22018-06-01 14:19:32 -0500480 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)
George Keishing16244c22019-01-31 16:16:14 +0000523
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
Michael Walsh23ac9b42019-02-20 16:46:04 -0600745 if len(pos_parms) > 0 and gp.is_dict(pos_parms[-1]):
George Keishing16244c22019-01-31 16:16:14 +0000746 # 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))
Michael Walsh23ac9b42019-02-20 16:46:04 -0600782 # 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)))
George Keishing16244c22019-01-31 16:16:14 +0000785
786 return command_string