subprocess.call() 如何与 shell=False 一起工作?

Posted

技术标签:

【中文标题】subprocess.call() 如何与 shell=False 一起工作?【英文标题】:How does subprocess.call() work with shell=False? 【发布时间】:2017-10-14 20:55:25 【问题描述】:

我正在使用 Python 的 subprocess 模块来调用一些 Linux 命令行函数。该文档将shell=True 参数解释为

如果shellTrue,则通过shell执行指定的命令

有两个例子,从描述性的角度来看,它们在我看来是一样的(即它们都调用了一些命令行命令),但其中一个使用 shell=True 而另一个没有

>>> subprocess.call(["ls", "-l"])
0

>>> subprocess.call("exit 1", shell=True)
1

我的问题是:

shell=True 相比,使用shell=False 运行命令有何作用? 我的印象是subprocess.callcheck_callcheck_output都必须通过shell执行参数。换句话说,它怎么可能通过shell执行参数?

获取以下示例也会有所帮助:

可以用shell=True 做的事情不能用shell=Trueshell=False 以及为什么不能完成。 反之亦然(虽然似乎没有这样的例子) shell=TrueFalse 无关紧要的事情以及为什么无关紧要

【问题讨论】:

你认为shell如何启动其他(非shell)程序?程序必须有一种方法可以在没有 shell 的情况下启动其他程序,否则 shell 也不能这样做(因为 shell 只是另一个程序)。 Shell 不是 UNIX 的基础。当你的系统第一次启动时,它会创建一个进程——PID 1——执行一个外部二进制文件,通常是/sbin/init。除非init 是一个shell 脚本,否则运行它时不涉及shell。 init 可以在不涉及任何 shell 的情况下同样运行其他程序;为此,它会复制自身(使用fork()),然后将新副本转换为不同的程序(使用execve());在此过程中的任何地方都没有贝壳。即使init 运行的一个shell,它也使用相同的进程。 ...所以,尊重 shebang 行 -- 无论是 #!/bin/sh#!/usr/bin/env 还是 #!/usr/bin/python -- 基本上是由内核完成的事情 作为一部分execve()的执行情况; shell 只是execve() 可以调用的另一个程序。 您只想要 Linux 的答案吗? Python subprocess.call() 也适用于 Windows。我问是因为这个问题可能是规范的,所以笼统地说是很好的。还可以更好地了解可移植性问题。 @smci,让我感到震惊的是,Linux 和 Windows 的答案完全不同,以至于单独的 Windows 问题能够更好地吸引具有特定领域专业知识的人。保持单个问题的范围更窄可以避免您有一个更好地涵盖方面 X 的答案,而另一个更好地涵盖方面 Y 的答案,因此没有/不能拥有一个理想的规范的答案。 【参考方案1】:

UNIX 程序通过以下三个调用或它们的派生/等价调用相互启动:

fork() - 创建自己的新副本。 exec() - 用不同的程序替换自己(如果你是副本,请这样做!)。 wait() - 等待另一个进程完成(可选,如果不在后台运行)。

因此,使用shell=False,您可以做到这一点(如下面的 Python 语法伪代码 - 如果不是阻塞调用,例如 subprocess.call(),则排除 wait()):

pid = fork()
if pid == 0: # we're the child process, not the parent
  execlp("ls", "ls", "-l", NUL);
else:
  retval = wait(pid) # we're the parent; wait for the child to exit & get its exit status

而对于shell=True,您可以这样做:

pid = fork()
if pid == 0:
  execlp("sh", "sh", "-c", "ls -l", NUL);
else:
  retval = wait(pid)

请注意,对于shell=False,我们执行的命令是ls,而对于shell=True,我们执行的命令是sh


也就是说:

subprocess.Popen(foo, shell=True)

完全一样:

subprocess.Popen(
  ["sh", "-c"] + ([foo] if isinstance(foo, basestring) else foo),
  shell=False)

也就是说,您执行/bin/sh 的副本,并指示/bin/sh 的副本将字符串解析为参数列表并执行ls -l 本身。


那么,为什么你使用shell=True

您正在调用内置的 shell。

例如,exit 命令实际上是 shell 本身的一部分,而不是外部命令。也就是说,这是一个 fairly small set of commands,它们很少在仅在单个 subprocess.call() 调用期间存在的 shell 实例的上下文中有用。

您有一些带有 shell 结构(即重定向)的代码,如果没有它,将难以模拟。

例如,如果您的命令是cat one two >three,则语法>three 是一个重定向:它不是cat 的参数,而是设置@987654347 的shell 指令@运行命令['cat', 'one', 'two']时。如果您不想自己处理重定向和管道,则需要一个 shell 来完成。

一个稍微复杂一点的例子是cat foo bar | baz。要在没有 shell 的情况下执行此操作,您需要自己启动管道的两端:p1 = Popen(['cat', 'foo', 'bar'], stdout=PIPE), p2=Popen(['baz'], stdin=p1.stdout)

您根本不在乎安全漏洞。

...好吧,这有点有点太强了,但不是很多。使用shell=True 是危险的。你不能这样做:Popen('cat -- %s' % (filename,), shell=True) 没有 shell 注入漏洞:如果你的代码曾经被包含 $(rm -rf ~)filename 调用,那么你将度过一个非常糟糕的一天。另一方面,['cat', '--', filename] 对所有可能的文件名都是安全的:文件名是纯粹的数据,不会被 shell 或其他任何东西解析为源代码。

可以在 shell 中编写安全脚本,但你需要小心。考虑以下几点:

filenames = ['file1', 'file2'] # these can be user-provided
subprocess.Popen(['cat -- "$@" | baz', '_'] + filenames, shell=True)

该代码是安全的(以及 - 就像让用户读取他们想要的任何文件永远一样安全),因为它从脚本代码带外传递文件名 - 但是之所以安全,是因为传递给 shell 的字符串是固定的和硬编码的,并且参数化的内容是外部变量(filenames 列表)。即便如此,它也只是在一定程度上是“安全的”——像 Shellshock 这样触发 shell 初始化的错误会像其他任何事情一样影响它。

【讨论】:

【参考方案2】:

我的印象是 subprocess.call 和 check_call 和 check_output 都必须通过 shell 执行参数。

不,子进程完全能够直接启动程序(通过操作系统调用)。它不需要外壳

shell=True 能做的事,shell=False 不能做的事

您可以将shell=False 用于任何简单地运行带有一些指定参数的可执行文件的命令。

如果您的命令使用 shell 功能,则必须使用 shell=True。这包括管道、| 或重定向,或者包含与 ;&&|| 等组合的复合语句。

因此,可以将shell=False 用于类似grep string file 的命令。但是,像grep string file | xargs something 这样的命令会,因为| 需要shell=True

因为 shell 具有强大的功能,python 程序员并不总是觉得很直观,除非你真的需要 shell 功能,否则使用shell=False 被认为是更好的做法。例如,管道并不是真正需要的,因为它们也可以使用子流程的 PIPE 功能来完成。

【讨论】:

“必须”有点强。每个 shell 功能——包括管道——都可以从 Python 中模拟——通过将Popen 实例串在一起、检查它们的返回值并有条件地执行未来的值等等。 @CharlesDuffy 是的。我只是添加了一句话来强调这一点。 (我可能会在这里暴露我的无知的真正深度,但是......)这是我困惑的很大一部分 - 子进程如何在没有 shell 的情况下运行像 grep string file 这样的东西 - 是不是grep 一个shell 程序吗? (特别是 Linux shell,例如它存在于 Windows)在没有 shell 的情况下运行它甚至意味着什么? @dkv,不,grep 不是外壳的一部分。它是一个外部 UNIX 可执行文件——它以 /usr/bin/grep 之类的形式存在于文件系统中,甚至可以在系统上不安装 shell 的情况下运行。 @dkv 如果你运行type grep,你会看到grep是一个可执行文件,通常位于/bin/grep。这与将 if 标识为 shell 命令的 type if 形成对比。

以上是关于subprocess.call() 如何与 shell=False 一起工作?的主要内容,如果未能解决你的问题,请参考以下文章

Python subprocess.call 与 cwd 不工作

subprocess.call 路径问题,如何解决?

如何防止subprocess.call打印返回代码?

如何使用 python 2.7.6 使 subprocess.call 超时?

如何捕获 subprocess.call 的输出

如何在 python 中杀死使用 subprocess.call 调用的子进程? [复制]