classification
Title: Add shell command helpers to subprocess module
Type: enhancement Stage: resolved
Components: Library (Lib) Versions: Python 3.4
process
Status: closed Resolution: out of date
Dependencies: Superseder:
Assigned To: Nosy List: alex, asvetlov, cvrebert, eric.araujo, eric.snow, martin.panter, ncoghlan, paul.moore, piotr.dobrogost, pitrou, r.david.murray, rhettinger
Priority: normal Keywords: patch

Created on 2011-10-21 06:00 by ncoghlan, last changed 2015-04-09 15:57 by r.david.murray. This issue is now closed.

Files
File name Uploaded Description Edit
issue13238_shell_helpers.diff ncoghlan, 2011-10-28 12:28 Diff between shell_helper branch and default branch review
b267e72c8c10.diff eric.araujo, 2011-11-15 16:02 review
Repositories containing patches
https://bitbucket.org/ncoghlan/cpython_sandbox#shell_helper
Messages (38)
msg146061 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-21 06:00
I've been doing a few systems administration tasks with Python recently, and shell command invocation directly via the subprocess module is annoyingly clunky (even with the new convenience APIs).

Since subprocess needs to avoid the shell by default for security reasons, I suggest we add the following simple helper functions to shutil:

    def call(cmd, *args, **kwds):
        if args or kwds:
            cmd = cmd.format(*args, **kwds)
        return subprocess.call(cmd, shell=True)

    def check_call(cmd, *args, **kwds):
        if args or kwds:
            cmd = cmd.format(*args, **kwds)
        return subprocess.check_call(cmd, shell=True)

    def check_output(cmd, *args, **kwds):
        if args or kwds:
            cmd = cmd.format(*args, **kwds)
        return subprocess.check_output(cmd, shell=True)


Initially posted at:
http://code.activestate.com/recipes/577891-simple-invocation-of-shell-commands/

Of course, even if others agree in principle, documentation and tests are still needed before this change can go anywhere.
msg146062 - (view) Author: Alex Gaynor (alex) * (Python committer) Date: 2011-10-21 06:01
These feel like a shell injection waiting to happen to me.
msg146064 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-21 06:36
And that's exactly the problem - a web developer's or security auditor's "shell injection" is a system administrator's "this language sucks".

These wrappers are the kind of thing you want for shell invocations when using Python as a replacement for a shell script or rewriting something that was originally written in Perl, but they're a terrible idea if anything you're interpolating came from an untrusted data source.

Currently, requiring "shell=True" in the arguments to the subprocess calls is considered a sufficient deterrent against people doing the wrong thing. I'm suggesting that requiring "import shutil" instead of "import subprocess" may be a similarly acceptable compromise that better serves the system administrators that choose to use Python for system automation tasks.
msg146065 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-21 06:41
Perhaps a better idea would be to use different names, so it's clearer at the point of invocation that the shell is being invoked (and hence shell injection attacks are a potential concern). For example:

  shell_call
  check_shell_call
  check_shell_output

That would make large applications easier to audit (just search for 'shell_') while still making life easier for sysadmins.
msg146069 - (view) Author: Chris Rebert (cvrebert) * Date: 2011-10-21 07:44
Is format() really the best choice here, considering that {}s already have a meaning in the shell?
msg146077 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-21 11:40
Of the 3 available options (mod style, string.Template and str.format), yes, str.format is the best choice.

If people want the shell meaning of the braces, they can escape them by doubling them up in the command string.
msg146085 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-10-21 13:54
Why not keeping these helpers in subprocess?
msg146148 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-21 23:02
Initially, because I was suggesting the names shadow the subprocess convenience functions so they *had* to live in a different namespace.

However, even after changing the names to explicitly include "shell", I'd like to keep them away from the general subprocess functionality - these wrappers are more convenient for shell operations than the subprocess ones, but it's that very convenience that makes them potentially dangerous in larger applications that may be interpolating data that untrusted users can manipulate.

Since the intended audience is system administrators working on shell-like operations, the shell utility module seems like an appropriate place for them. Both the "import shutil" and the "shell" in the names would then serve as red flags on a code review or security audit.
msg146150 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2011-10-21 23:13
Hum, in:

return_code = shellcmd.shell_call('ls -l {}', dirname)
listing = shellcmd.check_shell_output('ls -l {}', dirname)

...how do you know that dirname doesn't need some kind of escaping?
This is not only a security issue, but a bug. Even if security doesn't matter on your system, your script will still break and/or do unexpected things.

Also, I don't really understand how your recipe improves things. You're just saving one call to .format(). You would probably have the same saving by using the % operator.
msg146153 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-22 00:20
It's a flow thing. This idea was kicked off by the process of translating a large Perl script to Python and paying attention to what the translation made *worse*.

One of the big things it made worse was the translation of "qx" (quoted executable) strings. In Perl, those are almost as neat and tidy as if you were writing directly in the shell:

    qx|ls -l $dirname|

The thought process isn't "build this command and then execute it", it's "run this shell command".

Yes, you have to be careful that "dirname" is legal in the shell, but that usually isn't a big problem in practice, because dirname came from a previous listdir call, or you otherwise know that it's valid to interpolate it into the command (and if it isn't, then the bug lies in the way 'dirname' was populated, not in this operation).

Now, Python's never going to have implicit string interpolation, and that's fine - we have explicit interpolation instead. A direct translation of the above to idiomatic Python 2.7 code looks like the following:

    subprocess.check_output("ls -l {}".format(dirname), shell=True)

Now, if I'm doing system administration tasks (like the script I was translating), then I'm going to be doing that kind of thing a *lot*. I'm also likely to be a sysadmin rather than a programmer, so rather than going "Oh, I can write a helper function for this", my natural reaction is going to be "I'm going to use a language that doesn't get in my way so much".

This proposal is aimed directly at making Python a better language for systems administration by making shell invocation almost as easy as it is in Perl:

    shutil.check_shell_output("ls -l {}", dirname)

Heck, if someone really wanted to, they could even do:

    qx = shutil.check_shell_output
    qx("ls -l {}", dirname)

However, this is also why I *don't* want these methods in subprocess - they put the burden on the user to think about their data as if they were writing shell scripts, because that data is going to get passed straight to the shell for execution without any additional escaping. That's a reasonable idea for a shell utility in shutil, it's not reasonable for a general purpose subprocess manipulation utility in subprocess.
msg146154 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2011-10-22 00:41
> Yes, you have to be careful that "dirname" is legal in the shell, but
> that usually isn't a big problem in practice, because dirname came
> from a previous listdir call, or you otherwise know that it's valid to
> interpolate it into the command

I don't understand. os.listdir() doesn't escape filenames for you.

Having been bitten several times by almost-working shell commands which
would crash when one of the 10000 files being processed had a space in
it (ironically, I think that was in the Python source tree with some of
the old Mac directories), I think we really don't want to publish a
function which encourages people to pass unescaped arguments to the
shell.
msg146155 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-22 01:10
That's a fair point, but I think it actually *improves* the argument for better helper functions, since we can have them automatically invoke shlex.quote() on all of the arguments:

    def _shell_format(cmd, args, kwds):
        args = map(shlex.quote, args)
        kwds = {k:shlex.quote(v) for k, v in kwds}
        return cmd.format(*args, **kwds)

    def _invoke_shell(func, cmd, args, kwds):
        return func(_shell_format(cmd, args, kwds), shell=True)

    def format_shell_call(cmd, *args, kwds):
        return _shell_format(cmd, args, kwds)

    def shell_call(cmd, *args, **kwds):
        return _invoke_shell(subprocess.call, cmds, args, kwds)

    def check_shell_call(cmd, *args, **kwds):
        return _invoke_shell(subprocess.check_call, cmds, args, kwds)

    def check_shell_output(cmd, *args, **kwds):
        return _invoke_shell(subprocess.check_output, cmds, args, kwds)
msg146156 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2011-10-22 01:23
> That's a fair point, but I think it actually *improves* the argument
> for better helper functions

Agreed :)
msg146210 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-10-23 01:01
> [snip rationale about why shutil and not subprocess]
I’m convinced (with one nit: sh in the shutil name does not ring a security alarm for me, as I understand it as “shell-like conveniences in nice, dont-do-nasty-things-with-stings Python” :) but the shell in check_shell_call does warn).

Automatic call of shlex.quote is an argument in favor of the new helpers.
msg146211 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-10-23 01:02
s/stings/strings/
msg146299 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-24 12:42
Unfortunately, I don't think including implicit shlex.quote() calls is going to have the effect I was originally looking for:

>>> subprocess.call("du -hs ../py*", shell=True)
593M    ../py3k
577M    ../py3k_pristine
479M    ../python27
300M    ../python31
381M    ../python32
288K    ../python_swallowed_whole
0

>>> subprocess.call("du -hs {}".format(shlex.quote("../py*")), shell=True)
du: cannot access `../py*': No such file or directory
1

However, tinkering with this did provide some other "feels like using the shell" examples:

>>> subprocess.call("du -hs ~/devel", shell=True)
4.5G    /home/ncoghlan/devel
0

>>> subprocess.call(["du","-hs","~/devel"])
du: cannot access `~/devel': No such file or directory
1

(I'm using the existing subprocess calls rather than the proposed API, since I'm playing with this on the current hg tip without making any changes)
msg146345 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-25 08:52
Considering this further, I've realised that the idea of implicit quoting for this style of helper function is misguided on another level - the parameters to be interpolated may not even be strings yet, so attempting to quote them would fail:

>>> subprocess.call("exit {}".format(1), shell=True)
1
>>> shlex.quote(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ncoghlan/devel/py3k/Lib/shlex.py", line 285, in quote
    if _find_unsafe(s) is None:
TypeError: expected string or buffer

I'll note that none of these problems will be unique to the new convenience API - they're all a general reflection of the impedance mismatch between typical shell interfaces and filenames that can contain spaces.
msg146364 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-25 12:20
I discovered a couple of APIs that were moved from the commands module to the subprocess module in 3.0:
http://docs.python.org/dev/library/subprocess#subprocess.getstatusoutput

However, they have issues, especially on Windows: http://bugs.python.org/issue10197

So part of this patch would include deprecating those two interfaces in favour of the new shutil ones - the existing APIs break the subprocess promise of never invoking the shell implicitly.
msg146367 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-25 12:47
After a bit of thought, I realised I could use the string.Formatter API to implement a custom formatter for the shell command helpers that auto-escapes whitespace while leaving the other shell metacharacters alone (so you can still interpolate paths containing wildcards, etc).

People that want to bypass the autoescaping of whitespace can do the interpolation prior to the shell call, those that also want to escape shell metacharacters can use shlex.quote explicitly.

Diff can be seen here:
https://bitbucket.org/ncoghlan/cpython_sandbox/changeset/f19accc9a91b
msg146555 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-28 12:28
I realised I could use the convert_field() option in the custom formatter to choose between several interpolation quoting options:

  default - str + shutil.quote_ascii_whitespace
  !q - str + shlex.quote
  !u - unquoted (i.e. no conversion, str.format default behaviour)
  !s - str (as usual)
  !r - repr (as usual)

The most recent commit also exposes public APIs for the formatting aspects (shutil.quote_ascii_whitespace, shutil.shell_format, shutil.shell_format_map)
msg146556 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-28 12:31
Some examples:

>>> import shutil
>>> shutil.shell_call("du -hs {}", "../py*")
594M    ../py3k
579M    ../py3k_pristine
480M    ../python27
301M    ../python31
382M    ../python32
288K    ../python_swallowed_whole
0
>>> shutil.shell_call("du -hs {!q}", "../py*")
du: cannot access `../py*': No such file or directory
1
>>> shutil.shell_call("ls {}", "no file")
ls: cannot access no file: No such file or directory
2
>>> shutil.shell_call("ls {!u}", "no file")
ls: cannot access no: No such file or directory
ls: cannot access file: No such file or directory
2
msg146567 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-10-28 14:50
The custom formatter idea sounds brilliant.  Can you test that auto-escaping of spaces works well with glob patterns?
msg146570 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2011-10-28 15:21
>  default - str + shutil.quote_ascii_whitespace
>  !q - str + shlex.quote
>  !u - unquoted (i.e. no conversion, str.format default behaviour)

The default doesn't look very understandable to me. Why would you quote only some characters and not all of them?
msg146589 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-29 01:40
The first version I wrote *did* automatically invoke shlex.quote on all interpolated values, but that breaks wildcard handling. You can see that in the examples I posted above. With the default whitespace escaping (which allows spaces in filenames), wildcard matching still works (thus the list of directories matching the "../py*" pattern), but with full quoting it breaks (thus the "nothing named '../py*'" result).

So I figured the simplest default was "you can have spaces in your filenames, but otherwise you can do what you want". Now that I have the 'unquoted' conversion specifier, I'm open to tweaking that scheme to allow the same chars that shlex.quote does *plus* specifically the wildcard matching chars (i.e. '*', '?').
msg146603 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2011-10-29 11:16
>  With the default whitespace escaping (which allows spaces in
> filenames), wildcard matching still works (thus the list of
> directories matching the "../py*" pattern), but with full quoting it
> breaks (thus the "nothing named '../py*'" result).

My question is why it would be a good idea to make a difference between
whitespace and other characters. If you use a wildcard pattern,
generally it won't contain spaces at all, so you don't have to quote it.
If you are injecting a normal filename, noticing that whitespace gets
quoted may get you a false sense of security until somebody injects a
wildcard character that won't get quoted.

So what I'm saying is that a middleground between quoting and no quoting
is dangerous and not very useful.
msg146609 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-10-29 14:11
Yeah, I was thinking about this a bit more and realised that I'd rejected the "quote everything by default" approach before I had the idea of providing a custom conversion specifier to disable the implicit string conversion and quoting.

So perhaps a better alternative would be:

  default - str + shlex.quote
  !u - unquoted (i.e. normal str.format default behaviour)

When you have a concise way to explicitly bypass it, making the default behaviour as safe as possible seems like a good way to go.
msg146705 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-10-31 16:16
I’m not sure my question was well phrased.

If I have these files:
  spam.py
  ham.py
  foo bar.py

will a pattern of '*.py' match all of them with your functions, even the one with an embedded space?
msg147291 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-11-08 12:59
After working on the documentation updates to the subprocess module to emphasise the convenience functions, I've now changed my mind and think it makes sense to keep these additions in that module.

Now that the shell helpers will default to using shlex.quote() and require the "!u" conversion specifier to bypass that behaviour, and also will have distinct names from the ordinary non-shell based versions, my original concerns that lead to me wanting to keep them out of subprocess no longer apply. That means the general preference that subprocess be our "one stop shop" for explicit subprocess invocation can once again take precedence.

I'm actually planning to change the API a bit though, dropping "shell_format" and "shell_format_map" in favour of the following helper class:

    class ShellCommand:
        def __init__(self, command, *, **callkwds):
            self.command = command
            self.callkwds = callkwds
        def format(self, *args, **kwds):
            return _ShellFormatter().vformat(_fmt, args, kwds)
        def format_map(self, mapping):
            return _ShellFormatter().vformat(_fmt, (), mapping)
        def call(self, *args, **kwds):
            if args or kwds:
                cmd = _ShellFormatter().vformat(cmd, args, kwds)
            return subprocess.call(cmd, shell=True, **self.callkwds)
        def check_call(self, *args, **kwds):
            if args or kwds:
                cmd = _ShellFormatter().vformat(cmd, args, kwds)
            return subprocess.check_call(cmd, shell=True, **self.callkwds)
    def shell_output(cmd, *args, **kwds):
        if args or kwds:
            cmd = _ShellFormatter().vformat(cmd, args, kwds)
        data = subprocess.check_output(cmd, shell=True, universal_newlines=True)
        if data[-1:] == "\n":
            data = data[:-1]
        return data

The module level helpers would then just become re module style wrappers:

    def shell_call(command, *args, **kwds):
        return ShellCommand(command).shell_call(*args, **kwds)
    def check_shell_call(command, *args, **kwds):
        return ShellCommand(command).check_shell_call(*args, **kwds)
    def shell_output(command, *args, **kwds):
        return ShellCommand(command).shell_output(*args, **kwds)
msg147292 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-11-08 13:02
Éric, to answer your question, with the planned version of the new API, a "*.py" value interpolated with "{!u}" should indeed pick up all of those files, since the wildcard will be passed unmodified to the underlying shell, and all shells I am aware of will accept "foo bar.py" as a match for "*.py".
msg147686 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2011-11-15 16:11
>        def __init__(self, command, *, **callkwds):
Is the '*' marker needed?

>            self.callkwds = callkwds
These aren’t used in the module-level functions.  What is the use case?

If you forgive me for the nitpick, the docstrings have too much indenting.

> a "*.py" value interpolated with "{!u}" should indeed pick up all of those files,
> since the wildcard will be passed unmodified to the underlying shell
Great.

(This patch will also serve as a nice example of creating a string formatter.  I’ve seen the PyCon video about them, but I thought I was missing a piece since I didn’t see how you hook the format function to your custom formatter; it looks like the answer is that you don’t.)
msg147708 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2011-11-15 21:12
I like Nick's original idea for a handful of convenience functions but want to caution against adding a bunch of tools that start guessing at what you want.  Adding automatic wildcard expansion, shell quoting/splitting and whatnot can make the module more arcane and harder to learn and remember.  

Please adopt a conservation approach adding the minimal set that has been shown to be necessary.  We can take these tools away if they turn out to have been a bad idea.  It is much easier to add cruft than to take it away.   In the itertools module, a number of tools started their life as just a recipe in the docs and only became builtin after having proved their worth and having proved their API.
msg147739 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-11-16 00:01
Similar to what I did with walkdir, I plan to publish a shellcommand module on PyPI to give people a chance to look at the API and experiment with it without having to patch shutil in the standard library.

The only aspect I'm 100% sold on at the moment for the stdlib version is the use of a custom formatter to implement automatic invocation of shlex.quote(). It makes doing the right thing to avoid shell injection attacks (and whitespace problems in filenames) the path of least resistance: if you use shell_call/shell_check_call/shell_output and let them do the interpolation, then your call will be safe by default and the only thing you have to avoid is using an explicit conversion specifier that will bypass the quoting (i.e. '!a', '!r', '!s' or the custom '!u'). Since fear of encouraging shell injection vulnerabilities has historically been a big issue when it comes to shell invocation from the subprocess module, I think this part is mandatory. Once you do that, then '!u' is needed as a consequence, since sometimes you *will* want to pass unquoted values through in order to use standard numeric formatting options, or just to allow metacharacters to be interpreted by the shell, etc.

Interface wise, I'm still leaning towards just the _ShellFormatter class being private (due to its thread safety limitations), with ShellCommand and the three module level helpers making up the initial public API.

This isn't just speculation - by making the choice to use the *args and **kwds slots of the helper functions for string interpolation, it means they're no longer available for arguments to the Popen constructor.

In my own use case, one of the big things I want to do is invoke shell_call() with stdout and stderr redirected to a log file. Without ShellCommand exposed to let you customise the arguments to Popen, you end up having to essentially reimplement everything the helpers otherwise provide. With ShellCommand, you're able to still handle those less common use cases *without* having to reinvent the wheel on the formatting front:

    cmd = ShellCommand("cat {}", stdout=logfile, stderr=STDOUT)
    cmd.shell_call(cmd_arg)

For simple use cases where you *don't* want to tinker with the Popen args, though, the module level helpers will be the preferred option:

    data = shell_output("cat {}", cmd_arg).decode("utf-8")
msg149216 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-12-11 07:46
The PyPI package to prototype the API details is now available:

http://pypi.python.org/pypi/shell_command
http://shell-command.readthedocs.org
https://bitbucket.org/ncoghlan/shell_command/
msg154332 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-02-26 12:00
For a different take on this concept:
http://julialang.org/manual/running-external-programs/
msg157250 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-04-01 03:02
Bumping the target version for this issue to 3.4.

There's a lot of active development in the shell convenience function space at the moment, including my shell_command, Vinay Sajip's sarge, Kenneth Reitz's envoy and the possibility of doing something inspired by Julia's approach to this problem.

I'm not convinced enough that shell_command gets the security trade-offs right to push for this in the two months we have left before 3.3b1 (plus I have other things I want to work on). In the meantime, "pip install shell_command" is fairly straightforward (or similar for sarge, envoy or one of the other shell helper libraries).
msg174017 - (view) Author: Chris Rebert (cvrebert) * Date: 2012-10-28 09:40
Even more libraries in this vein:
http://plumbum.readthedocs.org/en/latest/
http://amoffat.github.com/sh/
msg191069 - (view) Author: Paul Moore (paul.moore) * (Python committer) Date: 2013-06-13 08:58
There's also https://pypi.python.org/pypi/sarge

One other thing I *often* want to do when scripting commands is to capture output, but provide some form of "progress" reporting (something like a dot per line of output, usually). That's annoyingly verbose with subprocess.
msg240346 - (view) Author: R. David Murray (r.david.murray) * (Python committer) Date: 2015-04-09 15:57
Having reviewed this issue, I don't see any point to leaving it open.  If any additional shell command helpers are added to the stdlib, it should be an ab-initio discussion based on what exists in the field, and start on python-ideas.  But given current sentiments about the stdlib, I doubt this is something we are likely to do in any case.
History
Date User Action Args
2015-04-09 15:57:34r.david.murraysetstatus: open -> closed
resolution: out of date
messages: + msg240346

stage: resolved
2013-08-04 10:58:43piotr.dobrogostsetnosy: + piotr.dobrogost
2013-06-13 08:58:38paul.mooresetnosy: + paul.moore
messages: + msg191069
2012-11-03 11:34:21asvetlovsetnosy: + asvetlov
2012-11-02 23:21:38martin.pantersetnosy: + martin.panter
2012-10-28 09:40:48cvrebertsetmessages: + msg174017
2012-07-13 05:46:06eric.snowsetnosy: + eric.snow
2012-04-05 01:59:21r.david.murraysetnosy: + r.david.murray
type: enhancement
2012-04-01 03:02:42ncoghlansetmessages: + msg157250
versions: + Python 3.4, - Python 3.3
2012-02-26 12:00:13ncoghlansetmessages: + msg154332
2011-12-11 07:46:42ncoghlansetmessages: + msg149216
2011-11-16 00:01:13ncoghlansetmessages: + msg147739
2011-11-15 21:12:45rhettingersetnosy: + rhettinger
messages: + msg147708
2011-11-15 16:11:02eric.araujosetmessages: + msg147686
2011-11-15 16:02:05eric.araujosetfiles: + b267e72c8c10.diff
2011-11-08 13:02:24ncoghlansetmessages: + msg147292
2011-11-08 12:59:24ncoghlansetmessages: + msg147291
title: Add shell command helpers to shutil module -> Add shell command helpers to subprocess module
2011-10-31 16:16:58eric.araujosetmessages: + msg146705
2011-10-29 14:11:01ncoghlansetmessages: + msg146609
2011-10-29 11:16:52pitrousetmessages: + msg146603
2011-10-29 01:40:14ncoghlansetmessages: + msg146589
2011-10-28 15:21:37pitrousetmessages: + msg146570
2011-10-28 14:50:19eric.araujosetmessages: + msg146567
2011-10-28 12:31:12ncoghlansetmessages: + msg146556
2011-10-28 12:28:14ncoghlansetfiles: + issue13238_shell_helpers.diff
keywords: + patch
messages: + msg146555
2011-10-25 12:47:31ncoghlansethgrepos: + hgrepo85
messages: + msg146367
2011-10-25 12:20:13ncoghlansetmessages: + msg146364
2011-10-25 08:52:25ncoghlansetmessages: + msg146345
2011-10-24 12:42:26ncoghlansetmessages: + msg146299
2011-10-23 01:02:39eric.araujosetmessages: + msg146211
2011-10-23 01:01:50eric.araujosetmessages: + msg146210
2011-10-22 01:23:18pitrousetmessages: + msg146156
2011-10-22 01:10:22ncoghlansetmessages: + msg146155
2011-10-22 00:41:41pitrousetmessages: + msg146154
2011-10-22 00:20:34ncoghlansetmessages: + msg146153
2011-10-21 23:13:23pitrousetnosy: + pitrou
messages: + msg146150
2011-10-21 23:02:53ncoghlansetmessages: + msg146148
2011-10-21 13:54:16eric.araujosetnosy: + eric.araujo
messages: + msg146085
2011-10-21 11:40:51ncoghlansetmessages: + msg146077
2011-10-21 07:44:26cvrebertsetnosy: + cvrebert
messages: + msg146069
2011-10-21 06:41:57ncoghlansetmessages: + msg146065
2011-10-21 06:36:58ncoghlansetmessages: + msg146064
2011-10-21 06:01:40alexsetnosy: + alex
messages: + msg146062
2011-10-21 06:00:12ncoghlancreate