为什么在Linux和Windows上subprocess.Popen()与shell=True的效果不同?
当我们在使用 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 个回答
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不太熟悉,所以不知道它为什么会有不同的表现。
来自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
时,直接丢弃所有的命令行参数。
其实在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]