1from __future__ import print_function, unicode_literals 2 3import logging 4import os 5import subprocess 6import sys 7 8import six 9 10from rbtools.utils.encoding import force_unicode 11 12 13def log_command_line(fmt, command): 14 """Log a command line. 15 16 Args: 17 fmt (unicode): 18 A format string to use for the log message. 19 20 command (list): 21 A command line in list form. 22 """ 23 # While most of the subprocess library can deal with bytes objects in 24 # command lines, list2cmdline can't. Decode each part if necessary. 25 logging.debug(fmt, subprocess.list2cmdline([ 26 force_unicode(part) for part in command 27 ])) 28 29 30def execute(command, 31 env=None, 32 cwd=None, 33 split_lines=False, 34 ignore_errors=False, 35 extra_ignore_errors=(), 36 with_errors=True, 37 none_on_ignored_error=False, 38 return_error_code=False, 39 log_output_on_error=True, 40 results_unicode=True, 41 return_errors=False): 42 """Execute a command and return the output. 43 44 Args: 45 command (unicode or list of unicode): 46 The command to execute. 47 48 env (dict, optional): 49 Environment variables to pass to the called executable. These will 50 be added to the current environment. 51 52 cwd (unicode, optional): 53 An optional working directory to change to before executing the 54 process. 55 56 split_lines (bool, optional): 57 Whether to return the output as a list of lines or a single string. 58 59 ignore_errors (bool, optional): 60 Whether to ignore errors. If ``False``, this will raise an 61 exception. 62 63 extra_ignore_errors (tuple, optional): 64 A set of errors to ignore even when ``ignore_errors`` is False. 65 This is used because some commands (such as diff) use non-zero 66 return codes even when the command was successful. 67 68 with_errors (bool, optional): 69 Whether to combine the output and error streams of the command 70 together into a single return value. This argument is mutually 71 exclusive with the ``return_errors`` argument. 72 73 none_on_ignored_error (bool, optional): 74 Whether to return ``None`` in the case that an error was ignored 75 (instead of the output of the command). 76 77 return_error_code (bool, optional): 78 Whether to include the exit status of the executed command in 79 addition to the output 80 81 log_output_on_error (bool, optional): 82 If ``True``, the output from the command will be logged in the case 83 that the command returned a non-zero exit code. 84 85 results_unicode (bool, optional): 86 If ``True``, the output will be treated as text and returned as 87 unicode strings instead of bytes. 88 89 return_errors (bool, optional): 90 Whether to return the content of the stderr stream. This argument 91 is mutually exclusive with the ``with_errors`` argument. 92 93 Returns: 94 This returns a single value, 2-tuple, or 3-tuple depending on the 95 arguments. 96 97 If ``return_error_code`` is True, the error code of the process will be 98 returned as the first element of the tuple. 99 100 If ``return_errors`` is True, the process' standard error stream will 101 be returned as the last element of the tuple. 102 103 If both of ``return_error_code`` and ``return_errors`` are ``False``, 104 then the process' output will be returned. If either or both of them 105 are ``True``, then this is the other element of the returned tuple. 106 """ 107 assert not (with_errors and return_errors) 108 109 if isinstance(command, list): 110 log_command_line('Running: %s', command) 111 else: 112 logging.debug('Running: %s', command) 113 114 new_env = os.environ.copy() 115 116 if env: 117 new_env.update(env) 118 119 # TODO: This can break on systems that don't have the en_US locale 120 # installed (which isn't very many). Ideally in this case, we could 121 # put something in the config file, but that's not plumbed through to here. 122 new_env['LC_ALL'] = 'en_US.UTF-8' 123 new_env['LANGUAGE'] = 'en_US.UTF-8' 124 125 if with_errors: 126 errors_output = subprocess.STDOUT 127 else: 128 errors_output = subprocess.PIPE 129 130 popen_encoding_args = {} 131 132 if results_unicode: 133 # Popen before Python 3.6 doesn't support the ``encoding`` parameter, 134 # so we have to use ``universal_newlines`` and then decode later. 135 if six.PY3 and sys.version_info.minor >= 6: 136 popen_encoding_args['encoding'] = 'utf-8' 137 else: 138 popen_encoding_args['universal_newlines'] = True 139 140 if sys.platform.startswith('win'): 141 # Convert all environment variables to the native string type, so that 142 # subprocess doesn't blow up on Windows. 143 new_env = dict( 144 (str(key), str(value)) 145 for key, value in six.iteritems(new_env) 146 ) 147 148 p = subprocess.Popen(command, 149 stdin=subprocess.PIPE, 150 stdout=subprocess.PIPE, 151 stderr=errors_output, 152 shell=False, 153 env=new_env, 154 cwd=cwd, 155 **popen_encoding_args) 156 else: 157 p = subprocess.Popen(command, 158 stdin=subprocess.PIPE, 159 stdout=subprocess.PIPE, 160 stderr=errors_output, 161 shell=False, 162 close_fds=True, 163 env=new_env, 164 cwd=cwd, 165 **popen_encoding_args) 166 167 data, errors = p.communicate() 168 169 # We did not specify `encoding` to Popen earlier, so we must decode now. 170 if results_unicode and 'encoding' not in popen_encoding_args: 171 data = force_unicode(data) 172 173 if split_lines: 174 data = data.splitlines(True) 175 176 if return_errors: 177 if split_lines: 178 errors = errors.splitlines(True) 179 else: 180 errors = None 181 182 rc = p.wait() 183 184 if rc and not ignore_errors and rc not in extra_ignore_errors: 185 if log_output_on_error: 186 logging.debug('Command exited with rc %s: %s\n%s---', 187 rc, command, data) 188 189 raise Exception('Failed to execute command: %s' % command) 190 elif rc: 191 if log_output_on_error: 192 logging.debug('Command exited with rc %s: %s\n%s---', 193 rc, command, data) 194 else: 195 logging.debug('Command exited with rc %s: %s', 196 rc, command) 197 198 if rc and none_on_ignored_error: 199 data = None 200 201 if return_error_code and return_errors: 202 return rc, data, errors 203 elif return_error_code: 204 return rc, data 205 elif return_errors: 206 return data, errors 207 else: 208 return data 209