如果您是Python程序员,您很可能有shell脚本编写经验。使用shell命令解决看似微不足道的任务并不少见。因此,熟悉如何从Python代码中高效地调用这些命令并了解它们的局限性很有必要。
在这篇短文中,我将讨论如何使用较旧的(尽管仍然相对常见)os.system和更新的subprocess。我将展示它们的一些潜在风险、局限性,并提供完整的使用示例。
如果您熟悉外部命令,那么对于小型任务来说,外部命令可能是一个有吸引力的选择,而python的替代方法意味着需要更多的学习/实现时间。但是,从长远来看,Python函数将为您节省时间和麻烦。
选项1:os.system(不推荐)Shell命令
下面我展示了一个简单的例子使用os.system命令打印用户提供的文件的第一行,shell使用head-n1命令。
#!/usr/bin/env python3
import os
# Define command and options wanted
command = "head -n 1 "
# Ask user for file name(s) - SECURITY RISK: susceptible to shell injection
filename = input("Please introduce name of file of interest:\n")
# Run os.system and save return_value
return_value = os.system(command+filename)
print('###############')
print('Return Value:', return_value)
这个os.system函数很好使用和解释:只需将在shell中使用的相同命令用作函数的输入。但是,它已被弃用,建议现在使用subprocess。
请注意,此函数只执行shell命令,结果将打印到标准输出,但函数返回的输出是返回值(如果运行正常,则为0,否则与0不同)。
1.1:shell注射敏感性
除其他缺点外,os.system直接在shell中执行命令,这意味着它容易受到shell注入(又称命令注入)的影响。
任何时候os.system正在接收未格式化的输入,例如当用户可以引入文件名时,如上面的示例所示。您可以尝试执行上面的脚本并给出以下输入:
dummy; touch harmful_file
这将导致shell:
head -n 1 dummy; touch harmful_file
因此,程序将按照预期先执行head-n1 dummy,但随后将执行命令touch hazardy_file以创建名为“hazardy_file”的文件。
假设这个空文件不是什么大威胁,但是你可以想象一个用户添加额外的命令来创建一个具有实际破坏目的的文件。这种shell注入还可以用于简单地传输或删除信息。例如,可以使用; rm -rf~,; rm -rf/或任何其他潜在危险的命令(请不要尝试这些!)。
选项2:subprocess.call(推荐用于Python<3.5)
2.1:运行外部shell命令的不安全方法
一个更好的选择是subprocess.call替代os.sys,函数subprocess.call返回返回值作为其输出。第一种简单的子流程方法是使用shell=True选项。这样,所需的命令也将在子shell中运行。不建议这样做,因为它具有与os.system一样的问题。 例如,下面的代码将与前面的代码等效os.system例子。
#!/usr/bin/env python3
import subprocess
# Define command and options wanted
command = "head -n 1 "
# Ask user for file name(s) - SECURITY RISK: susceptible to shell injection
filename = input("Please introduce name of file of interest:\n")
# Run subprocess.call and save return_value
return_value = subprocess.call(command+filename, shell=True)
print('###############')
print('Return value:', return_value)
2.2:运行外部命令的首选方式
一种更好的方式使用subprocess.call并通过设置shell=False(这是默认选项,因此不需要指定它)。然后,我们就可以打电话了subprocess.call(args),其中agrs[0]包含命令,args[1:]包含命令的所有额外选项。
#!/usr/bin/env python3
import subprocess
# Define command and options wanted
command = "head"
options = "-n 1"
# Ask user for file name(s) - now it's safe from shell injection
filename = input("Please introduce name(s) of file(s) of interest:\n")
# Create list with arguments for subprocess.call
args=[]
args.append(command)
args.append(options)
for i in filename.split():
args.append(i)
# Run subprocess.call and save return_value
return_value = subprocess.call(args)
print('###############')
print('Return value:', return_value)
2.3:获得标准输出:
subprocess.check_output
如前所述,subprocess.call具有返回值。但是,有时我们可能对shell命令返回的标准输出感兴趣。在这种情况下,我们可以利用subprocess.check_out。
为了使我们的脚本更健壮,我们可以添加try/exclude语句来处理check_out引发错误时的情况(在本例中,例如,没有找到文件时)。
如下图所示,我们可以使用subprocess.CalledProcessError在except子句中,以受控的方式处理错误并获取有关错误的信息。
#!/usr/bin/env python3
import subprocess
# Define command and options wanted
command = "head"
options = "-n 1"
# Ask user for file name(s) - now it's safe from shell injection
filename = input("Please introduce name(s) of file(s) of interest:\n")
# Create list with arguments for subprocess.check_output
args=[]
args.append(command)
args.append(options)
for i in filename.split():
args.append(i)
# Run subprocess.check_output and save command output
try:
output = subprocess.check_output(args)
# use decode function to convert to string
print('###############')
print('Output:', output.decode("utf-8"))
# If check_output returns an error:
except subprocess.CalledProcessError as error:
print('Error code:', error.returncode, '. Output:', error.output.decode("utf-8"))
选项3:subprocess.run(从python3.5开始推荐)
自python3.5以来,执行外部shell命令的推荐方法是使用subprocess.run。它比前面讨论的选项更安全、更友好。
默认情况下,此函数返回带有输入命令和返回代码的对象。通过使用选项capture_output=True,还可以很容易地获得标准输出,最后使用以下方法检索返回代码和命令输出:output.returncode及output.stdout。
#!/usr/bin/env python3
import subprocess
# Define command and options wanted
command = "head"
options = "-n 1"
# Ask user for file name(s) - now it's safe from shell injection
filename = input("Please introduce name(s) of file(s) of interest:\n")
# Create list with arguments for subprocess.check_output
args=[]
args.append(command)
args.append(options)
for i in filename.split():
args.append(i)
# Run subprocess.check_output and save command output
try:
output = subprocess.check_output(args)
# use decode function to convert to string
print('###############')
print('Output:', output.decode("utf-8"))
# If check_output returns an error:
except subprocess.CalledProcessError as error:
print('Error code:', error.returncode, '. Output:', error.output.decode("utf-8"))
关于Python中Shell命令的最后一点说明
如果您正在考虑使用本文讨论的任何方法来调用外部命令,那么可能需要考虑是否有一个标准的Python函数允许您执行相同的任务并避免创建新的进程。请注意,使用外部命令也会降低程序的跨平台友好性。
如果您熟悉外部命令,那么对于小型任务来说,外部命令可能是一个有吸引力的选择,而Python的替代方法意味着需要更多的学习/实现时间。
但是,从长远来看,坚持使用Python函数可以节省计算时间和麻烦,因此对于那些用标准Python库无法实现的任务,应该保存外部命令。
我希望本文能帮助您从python代码调用外部命令!