问题描述:

Why does Python's subprocess module expect the arguments as a list by default? Why isn't a string with spaces (similar to what you type into a terminal when running the command normally) the default input? There are plenty of sources explaining how to pass in the space delimited string of the command into subprocess, but it's less clear as to why the default isn't the other way around.

网友答案:

TL;DR Using the list bypasses the shell so that you don't need to worry about the shell interpreting a dynamically constructed command line in ways you did not intend.


Suppose you have a really simple command: echo foo. Here it is, using both a string and a list:

Popen("echo foo", shell=True)
Popen(["echo", "foo"])

Not much difference yet. Now suppose the argument contains quotes to protect whitespace and/or a shell pattern, echo "foo * bar":

Popen("echo \"foo   *    bar\"", shell=True)
Popen(["echo", "foo   *    bar"])

Yes, I could have used single quotes to avoid needing to escape the double quotes, but you can see the list form is starting to have an advantage. Now imagine I don't have a literal argument for the command, but that it is stored in a variable. Now which do you want to use...

This?

Popen('echo "%s"' % (x,), shell=True)

or this?

Popen(["echo", x])

If you answered "the first one", here's the value of x:

x = "\";rm -rf \""

The command you just executed was echo ""; rm -rf/"". You needed to make sure any special characters in the value of x were first escaped before incorporating it into the string you are building to pass to the shell.

Or you just use a list and avoid the shell altogether.

网友答案:

Forget all that I wrote - just read the relevant PEP yourself

https://www.python.org/dev/peps/pep-0324/

===============

My short guess - the no-shell list version is closer to the format that is eventually passed to the POSIX forking commands. It requires less manipulation. The shell string approach is something of a Windows legacy.

=====================

So you are asking why the shell=False case is the default?

On POSIX, with shell=False (default): In this case, the Popen class uses os.execvp() to execute the child program. args should normally be a sequence. A string will be treated as a sequence with the string as the only item (the program to execute).

On POSIX, with shell=True: If args is a string, it specifies the command string to execute through the shell. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional shell arguments.

'why' questions tend to be closed because they rarely have definitive answers, or they involve opinions, or history.

I'd suggest studying the subprocess.py code. I see for example a lot of calls to:

Popen(*popenargs, **kwargs)

It's init is:

def __init__(self, args, bufsize=-1, executable=None,
                 stdin=None, stdout=None, stderr=None,
                 preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
                 shell=False, cwd=None, env=None, universal_newlines=False,
                 startupinfo=None, creationflags=0,
                 restore_signals=True, start_new_session=False,
                 pass_fds=()):

As a keyword arg, shell has to have some default value; why not False?

I suspect that in the shell case it passes a whole string to some code that calls the shell. In the no-shell case it must pass a list. But we have to find that code.

There are 2 methods of call the subprocess, one for POSIX and the other Windows. In the POSIX case it appears to convert the string list, regardless whether shell is True or not It may be more nuanced than that, but this is the relevant code:

        """Execute program (POSIX version)"""

        if isinstance(args, (str, bytes)):
            args = [args]
        else:
            args = list(args)

        if shell:
            args = ["/bin/sh", "-c"] + args
            if executable:
                args[0] = executable
     ....
     self.pid = _posixsubprocess.fork_exec(
                        args, executable_list,...

In the windows shell case the args string is combined with cmd info:

    if shell:
            ....
            comspec = os.environ.get("COMSPEC", "cmd.exe")
            args = '{} /c "{}"'.format (comspec, args)
     hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                                     # no special security
                                     ....
相关阅读:
Top