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

在以下代码中:

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

and

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

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

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


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

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

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

如果您想要环境变量扩展和文件glob,请研究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...

查看这里的文档:


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

Where the interactions with the shell are nontrivial, you now require the reader and maintainer of the Python script (which may or may not be your future self) to understand both Python and shell script. Remember the Python motto "explicit is better than implicit"; even when the Python code is going to be somewhat more complex than the equivalent (and often very terse) shell script, you might be better off removing the shell and replacing the functionality with native Python constructs. Minimizing the work done in an external process and keeping control within your own code as far as possible is often a good idea simply because it improves visibility and reduces the risks of -- wanted or unwanted -- side effects.

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

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

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

with

subprocess.Popen(['command', '-with','-options', 'like this', 'and an argument'])

请注意,第一个参数是要传递给execvp()的字符串列表,而引用字符串和反斜杠转义shell元字符通常是不必要的(或有用或正确的)。 也许还参见何时将引号围绕一个壳变量?

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

顺便说一句,如果子流程包中的一个更简单的包装器满足了您的要求,那么您通常希望避免使用Popen。如果你有足够最新的Python,你可能应该使用subprocess.run。

使用check=True,如果您运行的命令失败,它将失败。 stdout =子流程。PIPE它将捕获命令的输出。 使用text=True(或者有点晦涩,使用同义词universal_newlines=True),它将输出解码为正确的Unicode字符串(在Python 3上,它只是系统编码中的字节)。

如果没有,对于许多任务,您需要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

你可以在完成前使用ps -auxf > 1,然后在完成后使用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/sh的pid。如果你调用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用户都知道它,听过它,并使用它。但我敢打赌,有很多人并不真正理解什么是壳。也许你也听说过/bin/bash,它们很相似。

shell的一个显著功能就是方便用户运行linux应用程序。由于sh或bash等shell程序,可以直接使用ls等命令,而不是/usr/bin/ls它将搜索ls的位置并为您运行它。

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

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

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