为啥带有 shell=True 的 subprocess.Popen() 在 Linux 和 Windows 上的工作方式不同?
Posted
技术标签:
【中文标题】为啥带有 shell=True 的 subprocess.Popen() 在 Linux 和 Windows 上的工作方式不同?【英文标题】:Why does subprocess.Popen() with shell=True work differently on Linux vs Windows?为什么带有 shell=True 的 subprocess.Popen() 在 Linux 和 Windows 上的工作方式不同? 【发布时间】:2010-11-18 04:42:10 【问题描述】:当使用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 下 args 应该发生什么,但它确实说,在 Unix 上,“如果 args 是一个序列,第一项指定命令字符串,任何附加项将被视为额外的 shell 参数。” 恕我直言,Windows 方式更好,因为它允许您将 Popen(arglist)
调用与 Popen(arglist, shell=True)
调用相同。
为什么这里有 Windows 和 Linux 的区别?
【问题讨论】:
最好包含您正在使用的 Python 版本,或者至少包含第 2 行或第 3 行。 【参考方案1】:实际上,在 Windows 上,当 shell=True
时,它确实使用 cmd.exe
- 它在 shell 参数前添加 cmd.exe /c
(它实际上查找 COMSPEC
环境变量,但默认为 cmd.exe
,如果不存在)。 (在 Windows 95/98 上,它使用中间 w9xpopen
程序来实际启动命令)。
所以奇怪的实现实际上是UNIX
一个,它执行以下操作(每个空格分隔不同的参数):
/bin/sh -c gcc --version
看起来正确的实现(至少在 Linux 上)应该是:
/bin/sh -c "gcc --version" gcc --version
因为这会从引用的参数中设置命令字符串,并成功传递其他参数。
来自-c
的sh
手册页部分:
Read commands from the command_string operand instead of from the standard input. Special parameter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.) set from the remaining argument operands.
这个补丁看起来很简单:
--- 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]
【讨论】:
太好了,谢谢大卫。我同意正确的实现,并且您的补丁看起来不错。在提交 Python 错误报告方面,您是否比我处于(更好的)位置——换句话说,您以前做过吗,还是我应该调查一下? 已添加 bugs.python.org/issue6689 - 如果您能关注它,在此发表评论等,那就太好了 谢谢!我已将自己添加到爱管闲事的名单中。 供参考,补丁被拒绝。看看是否需要修改文档可能是一个想法 - 我将把它留给感兴趣的一方【参考方案2】:来自 subprocess.py 来源:
在 UNIX 上,shell=True:如果 args 是一个字符串,它指定 要通过 shell 执行的命令字符串。如果 args 是一个序列, 第一项指定命令字符串,以及任何其他项 将被视为额外的 shell 参数。
在 Windows 上:Popen 类使用 CreateProcess() 来执行子进程 程序,它对字符串进行操作。如果 args 是一个序列,它将是 使用 list2cmdline 方法转换为字符串。请注意 并非所有 MS Windows 应用程序都以相同的方式解释命令行 方式:list2cmdline 是为使用相同的应用程序设计的 规则作为 MS C 运行时。
这没有回答原因,只是说明您看到了预期的行为。
“为什么”可能是在类 UNIX 系统上,命令参数实际上作为字符串数组传递给应用程序(使用 exec*
系列调用)。换句话说,调用进程决定了每个命令行参数的内容。而当您告诉它使用 shell 时,调用进程实际上只有机会将单个命令行参数传递给 shell 以执行:您想要执行的整个命令行、可执行文件名称和参数,作为单个字符串。
但在 Windows 上,整个命令行(根据上述文档)作为单个字符串传递给子进程。如果您查看CreateProcess API 文档,您会注意到它希望将所有命令行参数连接在一起形成一个大字符串(因此调用了list2cmdline
)。
另外,在类 UNIX 系统上,实际上是一个外壳可以做有用的事情,所以我怀疑差异的另一个原因是在 Windows 上,shell=True
什么都不做,这就是为什么它以您所看到的方式工作。使这两个系统行为相同的唯一方法是,在 Windows 上 shell=True
时,它只需删除所有命令行参数。
【讨论】:
Windows 上也有一个shell(通常是cmd.exe
),但是您上面引用的评论表明Python 在shell=True 时实际上并没有使用它(而是直接使用CreateProcess()
) .
谢谢——正如 Greg 所说,Windows 上肯定有一个 shell(cmd.exe 或 COMSPEC 中的那个)。它被 Popen 使用(虽然通过 CreateProcess)——参见 subprocess.py 源代码。因此,在我看来,子进程肯定应该让它们以相同的方式工作,以避免可移植性陷阱......
注:the docs has been updated since 2010【参考方案3】:
shell=True
的 UNIX 行为的原因与引用有关。当我们编写一个shell命令时,它会以空格分隔,所以我们必须引用一些参数:
cp "My File" "New Location"
当我们的参数包含引号时,这会导致问题,这需要转义:
grep -r "\"hello\"" .
有时我们可以得到awful situations,其中\
也必须转义!
当然,真正的问题是我们试图使用 one 字符串来指定 multiple 字符串。在调用系统命令时,大多数编程语言首先允许我们发送多个字符串来避免这种情况,因此:
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,所以我不知道它的行为方式或原因不同。
【讨论】:
以上是关于为啥带有 shell=True 的 subprocess.Popen() 在 Linux 和 Windows 上的工作方式不同?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 shell=True 和 shell=False 做同样的事情? [复制]
为啥不在 Python 中的 subprocess.Popen 中使用 `shell=True`? [复制]
为啥此输入类型=时间在某些带有“stepMismatch”的浏览器中无效:true?