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