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
参数解释为
如果shell为
True
,则通过shell执行指定的命令
有两个例子,从描述性的角度来看,它们在我看来是一样的(即它们都调用了一些命令行命令),但其中一个使用 shell=True
而另一个没有
>>> subprocess.call(["ls", "-l"])
0
>>> subprocess.call("exit 1", shell=True)
1
我的问题是:
与shell=True
相比,使用shell=False
运行命令有何作用?
我的印象是subprocess.call
和check_call
和check_output
都必须通过shell执行参数。换句话说,它怎么可能不通过shell执行参数?
获取以下示例也会有所帮助:
可以用shell=True
做的事情不能用shell=True
做
shell=False
以及为什么不能完成。
反之亦然(虽然似乎没有这样的例子)
shell=True
或 False
无关紧要的事情以及为什么无关紧要
【问题讨论】:
你认为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 不工作