为啥带有 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

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

来自-csh 手册页部分:

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?

带有 Hikari 的 Spring Data JPA:为啥 hikari 自动提交属性设置为“true”?

使用带有列表的 shell=True 时忽略 subprocess.call() 参数 [重复]