blob: c611d5c07d73774263a5666ac956c773c4ba5d1e [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 Walsh1429e122019-05-20 10:06:18 -050019import func_args as fa
Michael Walshd6901502017-11-14 12:58:37 -060020
21robot_env = gp.robot_env
22
Michael Walshafc53a22017-04-12 15:52:28 -050023if robot_env:
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
Michael Walshec01a6f2019-08-01 12:43:20 -050073 err_msg = gv.valid_value(cmd_buf)
Michael Walshc3b512e2017-02-20 15:59:01 -060074 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
Michael Walshc108e422019-03-28 12:27:18 -0500108 gp.gp_print(line)
Michael Walshc3b512e2017-02-20 15:59:01 -0600109 for line in sub_proc.stdout:
George Keishing36efbc02018-12-12 10:18:23 -0600110 try:
111 out_buf += line
112 except TypeError:
113 line = line.decode("utf-8")
114 out_buf += line
Michael Walshc3b512e2017-02-20 15:59:01 -0600115 if not print_output:
116 continue
Michael Walshc108e422019-03-28 12:27:18 -0500117 gp.gp_print(line)
Michael Walshc3b512e2017-02-20 15:59:01 -0600118 if print_output and not robot_env:
119 sys.stdout.flush()
120 sub_proc.communicate()
121 shell_rc = sub_proc.returncode
Michael Walsha3e2f532018-01-10 13:43:42 -0600122 if shell_rc != 0:
123 err_msg = "The prior shell command failed.\n"
Michael Walsh1429e122019-05-20 10:06:18 -0500124 err_msg += gp.sprint_var(shell_rc, gp.hexa())
Michael Walshcfe9fed2017-09-12 17:13:10 -0500125 if not print_output:
126 err_msg += "out_buf:\n" + out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600127
Michael Walsha3e2f532018-01-10 13:43:42 -0600128 if show_err:
Michael Walshc108e422019-03-28 12:27:18 -0500129 gp.print_error_report(err_msg)
Michael Walsha3e2f532018-01-10 13:43:42 -0600130 if not ignore_err:
131 if robot_env:
132 BuiltIn().fail(err_msg)
133 else:
134 raise ValueError(err_msg)
Michael Walshcfe9fed2017-09-12 17:13:10 -0500135
136 if return_stderr:
137 return shell_rc, out_buf, err_buf
138 else:
139 return shell_rc, out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600140
Michael Walshc3b512e2017-02-20 15:59:01 -0600141
Michael Walshc3b512e2017-02-20 15:59:01 -0600142def cmd_fnc_u(cmd_buf,
143 quiet=None,
144 debug=None,
145 print_output=1,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500146 show_err=1,
Michael Walsha3e2f532018-01-10 13:43:42 -0600147 return_stderr=0,
148 ignore_err=1):
Michael Walshc3b512e2017-02-20 15:59:01 -0600149 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,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500156 print_output=print_output, show_err=show_err,
Michael Walsha3e2f532018-01-10 13:43:42 -0600157 return_stderr=return_stderr, ignore_err=ignore_err)
Michael Walshc3b512e2017-02-20 15:59:01 -0600158
Michael Walshf41fac82017-08-02 15:05:24 -0500159
Michael Walshf41fac82017-08-02 15:05:24 -0500160def parse_command_string(command_string):
Michael Walshf41fac82017-08-02 15:05:24 -0500161 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:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500250 if isinstance(command_string_dict[key], str):
Michael Walshf41fac82017-08-02 15:05:24 -0500251 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
Michael Walsh21083d22018-06-01 14:19:32 -0500258
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,
Michael Walsh55abd1b2018-08-31 13:00:50 -0500284 print_output=None,
Michael Walsh21083d22018-06-01 14:19:32 -0500285 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,
Michael Walshfaafa9c2018-06-27 16:39:31 -0500292 return_stderr=0,
293 fork=0):
Michael Walsh21083d22018-06-01 14:19:32 -0500294 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
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500302 "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.
Michael Walsh21083d22018-06-01 14:19:32 -0500308 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.
Michael Walshfaafa9c2018-06-27 16:39:31 -0500356 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).
Michael Walsh21083d22018-06-01 14:19:32 -0500360 """
361
362 # Assign default values to some of the arguments to this function.
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500363 quiet = int(gm.dft(quiet, gp.get_stack_var('quiet', 0)))
Michael Walsh55abd1b2018-08-31 13:00:50 -0500364 print_output = int(gm.dft(print_output, not quiet))
Michael Walsh45fead42018-09-26 17:20:48 -0500365 show_err = int(show_err)
Michael Walsh22c3b6d2018-11-16 15:26:05 -0600366 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)))
Michael Walsh21083d22018-06-01 14:19:32 -0500369
Michael Walshec01a6f2019-08-01 12:43:20 -0500370 err_msg = gv.valid_value(command_string)
Michael Walsh21083d22018-06-01 14:19:32 -0500371 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.
Michael Walsh1429e122019-05-20 10:06:18 -0500384 allowed_shell_rcs = fa.source_to_object(allowed_shell_rcs)
Michael Walsh21083d22018-06-01 14:19:32 -0500385 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 = ""
Michael Walshfaafa9c2018-06-27 16:39:31 -0500412 if fork:
413 break
Michael Walsh21083d22018-06-01 14:19:32 -0500414 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:
George Keishing36efbc02018-12-12 10:18:23 -0600422 try:
423 err_buf += line
424 except TypeError:
425 line = line.decode("utf-8")
426 err_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500427 if not print_output:
428 continue
429 func_stdout += line
430 for line in sub_proc.stdout:
George Keishing36efbc02018-12-12 10:18:23 -0600431 try:
432 out_buf += line
433 except TypeError:
434 line = line.decode("utf-8")
435 out_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500436 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"
Michael Walsha750ed72018-07-24 16:18:09 -0500449 if quiet:
450 err_msg += gp.sprint_var(command_string)
Michael Walsh21083d22018-06-01 14:19:32 -0500451 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)
Michael Walsh1429e122019-05-20 10:06:18 -0500456 err_msg += gp.sprint_var(shell_rc, gp.hexa())
457 err_msg += gp.sprint_var(allowed_shell_rcs, gp.hexa())
Michael Walsh21083d22018-06-01 14:19:32 -0500458 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:
Michael Walshc108e422019-03-28 12:27:18 -0500463 func_stdout += gp.sprint_error_report(err_msg)
Michael Walsh21083d22018-06-01 14:19:32 -0500464 func_history_stdout += func_stdout
465 if attempt_num < max_attempts:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500466 func_history_stdout += gp.sprint_issuing("time.sleep("
467 + str(retry_sleep_time)
468 + ")")
Michael Walsh21083d22018-06-01 14:19:32 -0500469 time.sleep(retry_sleep_time)
470
471 if shell_rc not in allowed_shell_rcs:
472 func_stdout = func_history_stdout
473
Michael Walshc108e422019-03-28 12:27:18 -0500474 gp.gp_print(func_stdout)
Michael Walsh21083d22018-06-01 14:19:32 -0500475
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)
George Keishing16244c22019-01-31 16:16:14 +0000508
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
Michael Walsh58bb8fe2019-08-14 17:11:56 -0500696 Note to programmers who wish to write a wrapper to this function: If the
697 python version is less than 3.6, to get the options to be processed
698 correctly, the wrapper function must include a _stack_frame_ix_ keyword
699 argument to allow this function to properly re-order options:
George Keishing16244c22019-01-31 16:16:14 +0000700
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
Michael Walsh23ac9b42019-02-20 16:46:04 -0600730 if len(pos_parms) > 0 and gp.is_dict(pos_parms[-1]):
George Keishing16244c22019-01-31 16:16:14 +0000731 # 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.
Michael Walsh58bb8fe2019-08-14 17:11:56 -0500741 stack_frame_ix = options.pop('_stack_frame_ix_', 1)
742 if gm.python_version < gm.ordered_dict_version:
743 # Re-establish the original options order as specified on the
744 # original line of code. This function depends on correct order.
745 options = re_order_kwargs(stack_frame_ix, **options)
George Keishing16244c22019-01-31 16:16:14 +0000746 for key, value in options.items():
747 # Check for special values in options and process them.
748 if key == "arg_dashes":
749 arg_dashes = str(value)
750 delim = default_arg_delim(arg_dashes)
751 continue
752 if key == "arg_delim":
753 delim = str(value)
754 continue
755 # Format the options elements into the command string.
756 command_string += " " + arg_dashes + key
757 if value is not None:
758 command_string += delim
759 if re.match(r'^(["].*["]|[\'].*[\'])$', str(value)):
760 # Already quoted.
761 command_string += str(value)
762 else:
763 command_string += gm.quote_bash_parm(str(value))
Michael Walsh23ac9b42019-02-20 16:46:04 -0600764 # Finally, append the pos_parms to the end of the command_string. Use
765 # filter to eliminate blank pos parms.
766 command_string = ' '.join([command_string] + list(filter(None, pos_parms)))
George Keishing16244c22019-01-31 16:16:14 +0000767
768 return command_string