Ubuntu 18.04 上 Python 的 os.system 和 subprocess.check_output 中无法解释的 shell 命令反转义行为

Inexplicable shell command un-escaping behavior in Python's os.system and subprocess.check_output on Ubuntu 18.04

我对 Python 在 Ubuntu 18.04 上传递给 os.system 的命令中反斜杠的反斜杠感到困惑(在 CentOS 上一切正常)。考虑这个程序:

#!/usr/bin/env python
import os
import sys
import subprocess

def get_command(n):
    return "echo 'Should be %d backslashes: %s'" % (n, "\" * n)

print("")
print("Using os.system directly:")
print("")
for n in range(1, 5):
    os.system(get_command(n))

print("")
print("Using subprocess.check_output:")
print("")
for n in range(1, 5):
    sys.stdout.write(subprocess.check_output(get_command(n), shell=True).decode('utf-8'))

print("")
print("Writing the bash code to a script and using os.system on the script:")
print("")
for n in range(1, 5):
    with open('/tmp/script.sh', 'w') as f:
        f.write(get_command(n))
    os.system('/bin/bash /tmp/script.sh')

当我在 Ubuntu 18.04 运行 它时,我得到这个:

Using os.system directly:

Should be 1 backslashes: \
Should be 2 backslashes: \
Should be 3 backslashes: \
Should be 4 backslashes: \

Using subprocess.check_output:

Should be 1 backslashes: \
Should be 2 backslashes: \
Should be 3 backslashes: \
Should be 4 backslashes: \

Writing the bash code to a script and using os.system on the script:

Should be 1 backslashes: \
Should be 2 backslashes: \
Should be 3 backslashes: \\
Should be 4 backslashes: \\

注意它应该输出两个的地方输出一个反斜杠,应该输出三个或四个的地方输出两个反斜杠!

但是,在我的 CentOS 7 盒子上一切正常。在两台机器上 shell 都是 /bin/bash。这是脚本 python2.7 调用的 strace 输出,以防万一:https://gist.githubusercontent.com/mbautin/a97cfb6f880860f5fe6ce1474b248cfd/raw

我想从 Python 调用 shell 命令最安全的行为是将它们写入临时脚本文件!

虽然我同意这种行为很奇怪,但并非无法解释。这种行为是有原因的,与 Python 或 subprocess 无关。在 C 程序中可以看到完全相同的行为,使用 system 调用 OS (Linux) 与您的 Python 程序一样。

原因与您的 shell 有关,但不完全与 bash 有关。原因是当用shell=True调用os.system()subprocess.Popen()家族(包括subprocess.check_output())时。 documentation 指出 "On POSIX with shell=True, the shell defaults to /bin/sh." 因此,调用您的 echo 命令的 shell 不是 bash 即使是您的默认 shell 和您 运行 所在的 shell 您的 script/starting Python.

相反,您的命令由系统的 /bin/sh 执行。很长一段时间以来,几乎所有 Linux 版本都指向 /bin/bash(POSIX 兼容模式中的 运行),然而,最近这在某些发行版中发生了变化,其中 Ubuntu(但显然不是 CentOS,因为你在那里看不到相同的行为),现在 /bin/sh 指向 bin/dash

$ ll /bin/sh
lrwxrwxrwx 1 root root 4 sep 23 12:53 /bin/sh -> dash*

因此,您的脚本实际上是由 dash 而不是 bash 执行的。 "for efficiency"(请参阅提示中的 man dashdash 已选择在内部实现 echo 而不是使用 /bin/echo(由 bash 使用)。不幸的是, dash echo 不如 /bin/echo 强大,并且对字符串输入有不同的解释,即 dash echo 是否转义了一些反斜杠命令,实际上意味着 "swallows" 给你一个额外的反斜杠。

可以通过指定 -e 选项(参见 man echo)使 /bin/echo 以相同的方式运行,但不幸的是,[=25] 是不可能的=] 内置 echo 转义反斜杠。

现在,这就是您看到的原因。避免该问题的一个好方法是 依赖系统 shell 调用。如果它是单个命令,例如 echo,最好根本不要调用 shell,删除 shell=True 标志。或者,如果您需要某些 shell 特定功能,请自行控制 shell 的调用。并且,在这种特殊情况下,第三种方法是在执行时明确指向 /bin/echo,因为这确保使用 "standard" echo

#!/usr/bin/env python3
import sys
import subprocess
import shlex

def get_command(n):
    return "echo 'Should be {} backslahes: {}'".format(n, "\"*n)

print("")
print("Using subprocess.check_output:")
print("")
for n in range(1, 5):

    # Direct invocation:
    cmd = get_command(n)
    sys.stdout.write(subprocess.check_output(shlex.split(cmd)).decode())

    # Controlling invocation shell:
    bash_cmd = ['/bin/bash', '-c'] + [cmd]
    sys.stdout.write(subprocess.check_output(bash_cmd).decode())

    # Using shell=True but point to /bin/echo
    echo_cmd = '/bin/' + cmd
    sys.stdout.write(subprocess.check_output(echo_cmd, shell=True).decode())

请注意,当不使用 shell=True 时,命令应该是 list 而不是字符串。这可以是 shlex.split(),如图所示。

在这些方法中,如果某些参数有可能来自不受信任的来源,则首选第一种方法(直接 echo 调用),因为 security concerns。但是,在这种情况下,也不应使用 shlex.split(),因为它会带来相同的安全漏洞。