为什么在Linux和Windows上subprocess.Popen()与shell=True的效果不同?

26 投票
3 回答
24191 浏览
提问于 2025-04-15 13:29

当我们在使用 subprocess.Popen(args, shell=True) 来运行 "gcc --version"(这只是一个例子)时,在Windows上我们会看到这样的输出:

>>> from subprocess import Popen
>>> Popen(['gcc', '--version'], shell=True)
gcc (GCC) 3.4.5 (mingw-vista special r3) ...

这显示了我期待的版本信息,非常好。但是在Linux上,我们看到的是:

>>> from subprocess import Popen
>>> Popen(['gcc', '--version'], shell=True)
gcc: no input files

因为gcc没有收到 --version 这个选项。

文档没有明确说明在Windows上应该发生什么,但它提到,在Unix系统上,"如果args是一个序列,第一个项目指定命令字符串,任何额外的项目将被视为附加的shell参数。" 我个人认为Windows的方式更好,因为它允许你将 Popen(arglist) 的调用和 Popen(arglist, shell=True) 的调用视为相同。

为什么在Windows和Linux之间会有这样的区别呢?

3 个回答

-1

UNIX系统中使用shell=True的原因跟引号有关。当我们写一个命令时,命令会根据空格分开,所以有些参数需要用引号括起来:

cp "My File" "New Location"

这就会导致问题,特别是当我们的参数里面也有引号时,就需要进行转义处理:

grep -r "\"hello\"" .

有时候会出现一些很糟糕的情况,比如\也必须被转义!

其实,真正的问题在于我们试图用一个字符串来表示多个字符串。当调用系统命令时,大多数编程语言会避免这个问题,允许我们直接发送多个字符串,因此:

Popen(['cp', 'My File', 'New Location'])
Popen(['grep', '-r', '"hello"'])

有时候直接运行“原始”的shell命令会很方便;比如说,当我们从一个shell脚本或网站复制粘贴内容时,不想手动处理那些复杂的转义。这就是shell=True选项存在的原因:

Popen(['cp "My File" "New Location"'], shell=True)
Popen(['grep -r "\"hello\"" .'], shell=True)

我对Windows不太熟悉,所以不知道它为什么会有不同的表现。

5

来自subprocess.py的源代码:

在UNIX系统上,当设置shell=True时:如果args是一个字符串,它表示要通过shell执行的命令字符串。如果args是一个序列(比如列表),那么第一个项目是命令字符串,后面的项目会被当作额外的shell参数。

在Windows上:Popen类使用CreateProcess()来执行子程序,这个过程是基于字符串的。如果args是一个序列,它会通过list2cmdline方法转换成一个字符串。需要注意的是,并不是所有的Windows应用程序都以相同的方式解析命令行:list2cmdline是为那些遵循与MS C运行时相同规则的应用程序设计的。

这并没有回答为什么,只是说明你看到的行为是正常的。

至于“为什么”,可能是因为在类UNIX系统中,命令参数实际上是作为字符串数组传递给应用程序的(使用exec*系列的调用)。换句话说,调用进程决定每个命令行参数的内容。而当你告诉它使用shell时,调用进程实际上只能将一个完整的命令行参数传递给shell来执行:也就是你想执行的整个命令行,包括可执行文件名和参数,作为一个字符串。

但是在Windows上,整个命令行(根据上面的文档)是作为一个字符串传递给子进程的。如果你查看CreateProcess的API文档,你会发现它期望所有的命令行参数都被连接成一个大字符串(这就是为什么要调用list2cmdline)。

还有一个原因是,在类UNIX系统上确实有一个可以执行有用操作的shell,所以我猜测另一个差异的原因是,在Windows上,shell=True实际上没有任何作用,这就是为什么它的表现和你看到的那样。要让这两个系统的行为完全相同,唯一的方法就是在Windows上,当shell=True时,直接丢弃所有的命令行参数。

16

其实在Windows系统上,当你设置了 shell=True 时,它会使用 cmd.exe。这意味着它会在命令前加上 cmd.exe /c(实际上它会查找一个叫 COMSPEC 的环境变量,如果找不到就默认用 cmd.exe)。在Windows 95/98上,它会用一个叫 w9xpopen 的程序来启动命令。

所以,奇怪的实现其实是UNIX系统的,它的处理方式是这样的(每个空格分隔不同的参数):

/bin/sh -c gcc --version

看起来在Linux上正确的实现应该是:

/bin/sh -c "gcc --version" gcc --version

因为这样可以从引号中的参数设置命令字符串,并成功传递其他参数。

sh 的手册中关于 -c 的部分是这样说的:

从命令字符串操作数中读取命令,而不是从标准输入中读取。特殊参数0将从命令名称操作数中设置,位置参数($1, $2等)将从剩下的参数操作数中设置。

这个补丁似乎很简单就能解决问题:

--- subprocess.py.orig  2009-04-19 04:43:42.000000000 +0200
+++ subprocess.py       2009-08-10 13:08:48.000000000 +0200
@@ -990,7 +990,7 @@
                 args = list(args)

             if shell:
-                args = ["/bin/sh", "-c"] + args
+                args = ["/bin/sh", "-c"] + [" ".join(args)] + args

             if executable is None:
                 executable = args[0]

撰写回答