source: tailor/vcpx/shwrap.py @ 1426

Revision 1426, 8.8 KB checked in by lele@…, 6 years ago (diff)

Emit the redirection when capturing stderr

Line 
1# -*- mode: python; coding: iso-8859-1 -*-
2# :Progetto: vcpx -- Tiny wrapper around external command
3# :Creato:   sab 10 apr 2004 16:43:48 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8__docformat__ = 'reStructuredText'
9
10try:
11    # Python 2.4
12    from subprocess import Popen, PIPE, STDOUT
13except ImportError:
14    # Older snakes
15    from _process import Popen, PIPE, STDOUT
16
17class ReopenableNamedTemporaryFile:
18    """
19    This uses tempfile.mkstemp() to generate a secure temp file.  It
20    then closes the file, leaving a zero-length file as a placeholder.
21    You can get the filename with ReopenableNamedTemporaryFile.name.
22    When the ReopenableNamedTemporaryFile instance is garbage
23    collected or its shutdown() method is called, it deletes the file.
24
25    Copied from Zooko's pyutil.fileutil, http://zooko.com/repos/pyutil
26    """
27    def __init__(self, suffix=None, prefix=None, dir=None, text=None):
28        from tempfile import mkstemp
29        from os import close
30
31        fd, self.name = mkstemp(suffix, prefix, dir, text)
32        close(fd)
33
34    def __del__(self):
35        self.shutdown()
36
37    def shutdown(self):
38        from os import remove
39
40        remove(self.name)
41
42
43class ExternalCommand:
44    """Wrap a single command to be executed by the shell."""
45
46    DEBUG = False
47    """Print the output of the command, when not PIPEd to the caller."""
48
49    DRY_RUN = False
50    """Don't really execute the command."""
51
52    MAX_CMDLINE_LENGTH = 8000
53    """Don't execute commands longer than this number of characters."""
54
55    def __init__(self, command=None, cwd=None, nolog=False):
56        """
57        Initialize a ExternalCommand instance, specifying the command
58        to be executed and eventually the working directory.
59
60        The instance will use the logger ``tailor.shell``.
61        """
62
63        from logging import getLogger
64
65        self.command = command
66        """The command to be executed."""
67
68        self.cwd = cwd
69        """The working directory, go there before execution."""
70
71        self.exit_status = None
72        """Once the command has been executed, this is its exit status."""
73
74        self._last_command = None
75        """Last executed command."""
76
77        self.capture_stderr = False
78
79        if nolog:
80            self.log = False
81        else:
82            self.log = getLogger('tailor.shell')
83
84    def __str__(self):
85        """
86        Return a string representation of the command prefixed by working dir.
87        """
88
89        r = '$'+repr(self)
90        if self.cwd:
91            r = self.cwd + ' ' + r
92        if self.capture_stderr:
93            r = r + ' 2>&1'
94        return r
95
96    def __repr__(self):
97        """
98        Compute a reasonable shell-like representation of the external command.
99        """
100
101        result = []
102        needquote = False
103        for arg in self._last_command or self.command:
104            bs_buf = []
105
106            # Add a space to separate this argument from the others
107            result.append(' ')
108
109            needquote = (" " in arg) or ("\t" in arg)
110            if needquote:
111                result.append('"')
112
113            for c in arg:
114                if c == '\\':
115                    # Don't know if we need to double yet.
116                    bs_buf.append(c)
117                elif c == '"':
118                    # Double backspaces.
119                    result.append('\\' * len(bs_buf)*2)
120                    bs_buf = []
121                    result.append('\\"')
122                else:
123                    # Normal char
124                    if bs_buf:
125                        result.extend(bs_buf)
126                        bs_buf = []
127                    result.append(c)
128
129            # Add remaining backspaces, if any.
130            if bs_buf:
131                result.extend(bs_buf)
132
133            if needquote:
134                result.extend(bs_buf)
135                result.append('"')
136
137        return ''.join(result)
138
139    def execute(self, *args, **kwargs):
140        """Execute the command, avoiding too long command line."""
141
142        from cStringIO import StringIO
143
144        if kwargs.get('stderr'):
145            self.capture_stderr = True
146        else:
147            self.capture_stderr = False
148
149        if len(args) == 1 and type(args[0]) == type([]):
150            allargs = list(args[0])
151        else:
152            allargs = list(args)
153
154        maxlen = self.MAX_CMDLINE_LENGTH
155        if maxlen is None or len(allargs) < 2:
156            return self._execute(allargs, **kwargs)
157
158        startlen = len(' '.join(self.command))
159        allout = None
160        allerr = None
161        while allargs:
162            thisrun = []
163            clen = startlen
164            pop = allargs.pop
165            append = thisrun.append
166            while allargs and clen<maxlen:
167                thisarg = pop(0)
168                clen += len(thisarg)+1
169                append(thisarg)
170            thisout, thiserr = self._execute(*thisrun, **kwargs)
171            if thisout is not None:
172                if allout is None:
173                    allout = StringIO()
174                allout.write(thisout.read())
175            if thiserr is not None:
176                if allerr is None:
177                    allerr = StringIO()
178                allerr.write(thiserr.read())
179            if self.exit_status:
180                break
181        if allout is not None:
182            allout.seek(0)
183        if allerr is not None:
184            allerr.seek(0)
185        return allout, allerr
186
187    def _execute(self, *args, **kwargs):
188        """Execute the command."""
189
190        from sys import stderr
191        from locale import getpreferredencoding
192        from os import environ, getcwd
193        from os.path import isdir
194        from cStringIO import StringIO
195        from errno import ENOENT
196
197        self.exit_status = None
198
199        self._last_command = [chunk % kwargs for chunk in self.command]
200        if len(args) == 1 and type(args[0]) == type([]):
201            self._last_command.extend(args[0])
202        else:
203            self._last_command.extend(args)
204
205        if self.log: self.log.info(self)
206
207        if self.DRY_RUN:
208            return
209
210        cwd = kwargs.setdefault('cwd', self.cwd or getcwd())
211        if not isdir(cwd):
212            raise OSError(ENOENT, "Working directory does not exist", cwd)
213
214        if self.log: self.log.debug("Executing %r (%r)", self, cwd)
215
216        if not kwargs.has_key('env'):
217            env = kwargs['env'] = {}
218            env.update(environ)
219
220            for v in ['LANG', 'TZ', 'PATH']:
221                if kwargs.has_key(v):
222                    env[v] = kwargs[v]
223            # Override also LC_ALL that has a higher priority over LANG,
224            # and LC_MESSAGES as well.
225            if kwargs.has_key('LANG'):
226                env['LC_ALL'] = kwargs['LANG']
227                env['LC_MESSAGES'] = kwargs['LANG']
228
229        input = kwargs.get('input')
230        output = kwargs.get('stdout')
231        error = kwargs.get('stderr')
232
233        # When not in debug, redirect stderr and stdout to /dev/null
234        # when the caller didn't ask for them.
235        if not self.DEBUG:
236            try:
237                from os import devnull
238            except ImportError:
239                devnull = '/dev/null'
240            if output is None:
241                output = open(devnull, 'w')
242            if error is None:
243                error = open(devnull, 'w')
244        try:
245            process = Popen(self._last_command,
246                            stdin=input and PIPE or None,
247                            stdout=output,
248                            stderr=error,
249                            env=kwargs.get('env'),
250                            cwd=cwd,
251                            universal_newlines=True)
252        except OSError, e:
253            if e.errno == ENOENT:
254                raise OSError("%r does not exist!" % self._last_command[0])
255            else:
256                raise
257
258        if input and isinstance(input, unicode):
259            encoding = getpreferredencoding()
260            if self.log:
261                self.log.warning("Using default %s encoding, ignoring errors; "
262                                 "caller should use repository's encoding and "
263                                 "pass an already encoded input" % encoding)
264            input = input.encode(encoding, 'ignore')
265
266        out, err = process.communicate(input=input)
267
268        self.exit_status = process.returncode
269        if not self.exit_status:
270            if self.log: self.log.info("[Ok]")
271        else:
272            if self.log: self.log.warning("[Status %s]", self.exit_status)
273
274        # For debug purposes, copy the output to our stderr when hidden above
275        if self.DEBUG:
276            if out and output == PIPE:
277                stderr.write('Output stream:\n')
278                stderr.write(out)
279            if err and error == PIPE:
280                stderr.write('Error stream:\n')
281                stderr.write(err)
282
283        if out is not None:
284            out = StringIO(out)
285        if err is not None:
286            err = StringIO(err)
287
288        return out, err
Note: See TracBrowser for help on using the repository browser.