'shell=True'在子流程

我正在用subprocess模块调用不同的进程。然而,我有一个问题。

在下列代码中:

callProcess = subprocess.Popen(['ls', '-l'], shell=True)

而且

callProcess = subprocess.Popen(['ls', '-l']) # without shell

这两个工作。在阅读文档后,我知道shell=True意味着通过shell执行代码。也就是说,如果不存在,这个过程将直接启动。

那么对于我的情况,我应该选择什么呢?我需要运行一个进程并获得它的输出。从壳内或壳外调用有什么好处呢?

275573 次浏览

不通过shell调用的好处是,您不会调用一个“神秘程序”。在POSIX上,环境变量SHELL控制作为“shell”调用的二进制文件。在Windows上,没有bourne shell的后代,只有cmd.exe。

因此调用shell调用用户选择的程序,并且依赖于平台。一般来说,避免通过shell调用。

通过shell调用确实允许您根据shell的通常机制展开环境变量和文件glob。在POSIX系统上,shell将文件glob扩展为一个文件列表。在Windows上,一个文件glob(例如,“*.*”)不会被shell扩展(但是命令行上的环境变量会被cmd.exe扩展)。

如果您想要环境变量扩展和文件globb,请研究1992-ish对通过shell执行子程序调用的网络服务的ILS攻击。例子包括涉及ILS的各种sendmail后门。

总之,使用shell=False

通过shell执行程序意味着传递给程序的所有用户输入都将根据所调用shell的语法和语义规则进行解释。在最好的情况下,这只会给用户带来不便,因为用户必须遵守这些规则。例如,包含特殊shell字符(如引号或空格)的路径必须转义。在最坏的情况下,它会导致安全泄漏,因为用户可以执行任意程序。

shell=True有时可以方便地使用特定的shell特性,如字分割或参数展开。然而,如果需要这样的特性,请使用其他提供给你的模块(例如os.path.expandvars()用于参数展开或shlex用于单词分割)。这意味着更多的工作,但避免了其他问题。

简而言之:无论如何要避免shell=True

这里展示了一个Shell=True可能出错的示例

>>> from subprocess import call
>>> filename = input("What file would you like to display?\n")
What file would you like to display?
non_existent; rm -rf / # THIS WILL DELETE EVERYTHING IN ROOT PARTITION!!!
>>> call("cat " + filename, shell=True) # Uh-oh. This will end badly...

检查这里的文档:subprocess.call ()

这里的其他答案充分解释了subprocess文档中也提到的安全警告。但是除此之外,启动一个shell来启动你想要运行的程序的开销通常是不必要的,而且对于你实际上不使用任何shell功能的情况来说是愚蠢的。此外,额外隐藏的复杂性应该吓到你,特别是如果你不是很熟悉shell或它提供的服务。

在与shell的交互非常重要的地方,您现在需要Python脚本的读者和维护者(可能是也可能不是您未来的自己)同时理解Python和shell脚本。记住Python格言“明胜于暗”;;,即使Python代码将比等效的(通常非常简洁)shell脚本更复杂,您可能会更好地删除shell并使用本机Python构造替换功能。尽量减少在外部进程中完成的工作并在您自己的代码中保持控制通常是一个好主意,因为它提高了可见性并降低了(想要的或不想要的)副作用的风险。

通配符展开、变量插值和重定向都很容易用原生Python结构替换。对于部分或全部无法用Python合理重写的复杂shell管道,也许可以考虑使用shell。您仍然应该确保了解性能和安全影响。

在简单的情况下,为了避免shell=True,只需替换

subprocess.Popen("command -with -options 'like this' and\\ an\\ argument", shell=True)

subprocess.Popen(['command', '-with','-options', 'like this', 'and an argument'])
注意第一个参数是一个要传递给execvp()的字符串列表,并且引号字符串和反斜杠转义shell元字符通常是不必要的(或有用的,或正确的)。 也许还可以参见什么时候在一个shell变量周围包装引号?

如果你不想自己弄清楚,shlex.split()函数可以为你做这件事。它是Python标准库的一部分,但是当然,如果您的shell命令字符串是静态的,您可以在开发期间只运行一次,并将结果粘贴到脚本中。

顺便说一句,如果subprocess包中的一个更简单的包装器实现了你想要的功能,你通常希望避免使用Popen。如果你有足够最新的Python,你可能应该使用subprocess.run

  • 对于check=True,如果你运行的命令失败,它将失败。
  • 使用stdout=subprocess.PIPE,它将捕获命令的输出。
  • 使用text=True(或者有点晦涩的同义词universal_newlines=True),它将输出解码为适当的Unicode字符串(在Python 3上,它只是系统编码中的bytes)。

如果不是,对于许多任务,你希望check_output从命令获取输出,同时检查它是否成功,或者check_call如果没有输出要收集。

我将引用David Korn的一句话作为结束:“编写一个可移植的shell比编写一个可移植的shell脚本更容易。”即使subprocess.run('echo "$HOME"', shell=True)也不能移植到Windows。

>>> import subprocess
>>> subprocess.call('echo $HOME')
Traceback (most recent call last):
...
OSError: [Errno 2] No such file or directory
>>>
>>> subprocess.call('echo $HOME', shell=True)
/user/khong
0

将shell参数设置为真值会导致子进程生成一个中间shell进程,并告诉它运行该命令。换句话说,使用中间shell意味着在运行命令之前处理命令字符串中的变量、glob模式和其他特殊shell特性。在本例中,$HOME在echo命令之前被处理。实际上,这就是shell扩展命令的情况,而ls -l命令被认为是一个简单的命令。

来源:子流程模块

让我们假设您正在使用shell=False并以列表的形式提供命令。一些恶意用户试图注入'rm'命令。 您将看到,'rm'将被解释为一个参数,有效地'ls'将尝试查找名为'rm'

的文件
>>> subprocess.run(['ls','-ld','/home','rm','/etc/passwd'])
ls: rm: No such file or directory
-rw-r--r--    1 root     root          1172 May 28  2020 /etc/passwd
drwxr-xr-x    2 root     root          4096 May 29  2020 /home
CompletedProcess(args=['ls', '-ld', '/home', 'rm', '/etc/passwd'], returncode=1)

shell=False在默认情况下不是安全的,如果你没有正确地控制输入。你仍然可以执行危险的命令。

>>> subprocess.run(['rm','-rf','/home'])
CompletedProcess(args=['rm', '-rf', '/home'], returncode=0)
>>> subprocess.run(['ls','-ld','/home'])
ls: /home: No such file or directory
CompletedProcess(args=['ls', '-ld', '/home'], returncode=1)
>>>

我在容器环境中编写我的大部分应用程序,我知道哪个shell正在被调用,我不接受任何用户输入。

所以在我的用例中,我没有看到安全风险。而且,创建长串命令要容易得多。希望我没说错。

上面的回答正确地解释了它,但不够直接。 让我们使用ps命令来看看会发生什么
import time
import subprocess


s = subprocess.Popen(["sleep 100"], shell=True)
print("start")
print(s.pid)
time.sleep(5)
s.kill()
print("finish")

运行它,然后显示

start
832758
finish

你可以在finish之前使用ps -auxf > 1,在finish之后使用ps -auxf > 2。这是输出

< >强1 < / >强

cy         71209  0.0  0.0   9184  4580 pts/6    Ss   Oct20   0:00  |       \_ /bin/bash
cy        832757  0.2  0.0  13324  9600 pts/6    S+   19:31   0:00  |       |   \_ python /home/cy/Desktop/test.py
cy        832758  0.0  0.0   2616   612 pts/6    S+   19:31   0:00  |       |       \_ /bin/sh -c sleep 100
cy        832759  0.0  0.0   5448   532 pts/6    S+   19:31   0:00  |       |           \_ sleep 100

看到了吗?而不是直接运行sleep 100。它实际上运行/bin/sh。它打印出来的pid实际上是/bin/shpid。如果你调用s.kill(),它会杀死/bin/sh,但sleep仍然存在。

2 <强> < / >强

cy         69369  0.0  0.0 533764  8160 ?        Ssl  Oct20   0:12  \_ /usr/libexec/xdg-desktop-portal
cy         69411  0.0  0.0 491652 14856 ?        Ssl  Oct20   0:04  \_ /usr/libexec/xdg-desktop-portal-gtk
cy        832646  0.0  0.0   5448   596 pts/6    S    19:30   0:00  \_ sleep 100

所以下一个问题是,/bin/sh能做什么?每个linux用户都知道它,听过它,并使用它。但我敢打赌,有很多人真的不明白shell是什么。也许你也听到过/bin/bash,它们是相似的。

shell的一个显著功能就是方便用户运行linux应用程序。由于shell程序如shbash,你可以直接使用像ls这样的命令,而不是/usr/bin/ls。它会搜索ls所在的位置并为你运行它。

另一个函数是将$后面的字符串解释为环境变量。您可以比较这两个python脚本来自己找出答案。

subprocess.call(["echo $PATH"], shell=True)
subprocess.call(["echo", "$PATH"])

最重要的是,它使linux命令可以以脚本的形式运行。如if else是由shell引入的。它不是原生的Linux命令