执行 Shell 命令

PythonBeginner
立即练习

简介

在运维工作中,即使编写的是 Python 程序,你通常仍然需要调用现有的系统命令。因此,理解 Python 的进程执行工具非常重要。

subprocess 模块可以让 Python 启动其他程序、捕获其输出,并安全地检查程序是否成功或失败。它是连接 Python 代码与 Shell 命令的标准方式。

在本实验中,你将学习如何:

  1. 导入 subprocess 模块。
  2. 使用 Python 运行简单的 Linux 命令(ls -l)。
  3. 捕获命令输出,以便后续处理。
  4. 将输出从字节解码为可读字符串。
  5. 在命令失败时妥善处理错误。

你将在 Linux 环境的 ~/project 目录中完成操作。

导入 Subprocess

在本步骤中,你将创建一个 Python 脚本,并导入用于与系统 Shell 交互的模块。

subprocess 模块属于 Python 标准库,因此不需要额外安装。它可以让 Python 脚本与底层操作系统通信。

这是 Python 代码与现有 Unix 工具之间的标准桥梁。在运维工作中,这种桥接方式很重要,因为你很少会一次性替换所有 Shell 命令。更常见的做法是让 Python 充当协调者:调用系统工具、捕获工具结果,并在其基础上添加更高层的逻辑。

首先,确认当前位于正确的目录:

cd ~/project

现在,使用 nano 编辑器或你喜欢的其他方法创建名为 list_files.py 的 Python 文件:

nano list_files.py

在文件顶部添加以下内容,以导入该模块:

import subprocess

print("Module imported successfully")

保存文件并退出编辑器(在 nano 中,按 Ctrl+X,然后按 Y,最后按 Enter)。

现在运行脚本,确认一切正常:

python3 list_files.py

你应该会看到消息 "Module imported successfully"。

运行命令

在本步骤中,你将修改脚本,使其真正执行 Linux 命令。我们将使用 subprocess.run() 函数。该函数从 Python 3.5 开始提供,是调用子进程的推荐方式。

当 Python 启动另一个程序时,系统会创建一个独立的进程。这个子进程拥有自己的退出代码、标准输出和标准错误,就像你直接在 Shell 中运行命令时一样。理解这种进程边界后,后续的错误处理会更容易理解。

再次打开 list_files.py:

nano list_files.py

将文件修改为以下内容。这里使用 ls -l 命令,以详细信息格式列出当前目录中的文件:

import subprocess

## Run the 'ls -l' command
## The command and its arguments are passed as a list of strings
subprocess.run(["ls", "-l"])

注意,我们以列表形式传入命令:["ls", "-l"]。与传入单个字符串相比,这种方式更安全、更清晰,因为它可以避免 Shell 注入以及文件名中空格导致的问题。

保存并退出文件。

现在运行脚本:

python3 list_files.py

你应该会在终端中直接看到 ls -l 命令的输出。这是因为默认情况下,subprocess.run 会将命令输出发送到标准输出,也就是你的屏幕。

捕获输出

上一步中,命令输出被直接打印到屏幕上。但在自动化脚本中,你通常希望将输出捕获到变量中,以便 Python 代码进行处理,例如解析文件名或检查错误。

这就是单纯运行命令与将命令集成到程序之间的区别。输出被捕获为数据后,脚本就可以根据这些数据做出判断,而不必只把结果留在终端上供人查看。

要实现这一点,需要使用 capture_output=True 参数。

再次打开 list_files.py:

nano list_files.py

更新代码,以捕获命令结果:

import subprocess

## Run the command and capture the output
result = subprocess.run(["ls", "-l"], capture_output=True)

## Print the captured result object
print(result)

保存并退出。

运行脚本:

python3 list_files.py

你会看到类似下面的输出:

CompletedProcess(args=['ls', '-l'], returncode=0, stdout=b'total 0\n-rw-rw-r-- 1 labex labex 0 ...', stderr=b'')

这是一个 CompletedProcess 对象。注意其中的 stdout=b'...'。这里的 b 表示字节(bytes)。这意味着 Python 捕获的是计算机实际看到的原始数据,而不是人类可读的文本(字符串)。下一步将把这些数据转换为人类可读的文本。

解码输出

由于捕获的输出是字节,因此需要将其解码为标准的 Python 字符串(UTF-8),这样才能读取和使用它。

字节与文本的区别是 Python 中一个基础且重要的概念。计算机最终处理和传输的是原始字节,而人类通常需要的是文本。自动化程序中的许多疑难错误,都源于忘记当前数据究竟是字节还是文本。

再次打开 list_files.py:

nano list_files.py

更新代码,访问 stdout 属性并对其进行解码:

import subprocess

## Run the command and capture the output
result = subprocess.run(["ls", "-l"], capture_output=True)

## Decode the bytes to a string
output_string = result.stdout.decode("utf-8")

## Print the clean string
print("--- Command Output ---")
print(output_string)

保存并退出。

运行脚本:

python3 list_files.py

现在,你应该会看到格式清晰的 ls -l 命令输出。它看起来就像直接在 Shell 中运行该命令时的结果,但这次输出是由 Python 打印的!

注意: 另一种方法是向 subprocess.run() 传入 text=True(或 universal_newlines=True),让 Python 自动完成解码。不过,理解 .decode() 方法对于处理二进制数据非常重要。

处理错误

如果命令失败,会发生什么?例如,尝试列出一个不存在的目录时就会失败。完善的脚本需要处理这类错误。

在类 Unix 系统中,命令失败通常通过非零退出状态来表示。subprocess.run(..., check=True) 会将这种操作系统约定转换为 Python 异常,使脚本可以使用常规的 Python 控制流进行响应。

下面创建一个名为 handle_error.py 的新脚本来演示这一点。

nano handle_error.py

添加以下代码。我们将尝试列出不存在的目录 /nonexistent。同时使用 check=True,这样当命令返回非零退出代码(表示失败)时,Python 会抛出 CalledProcessError 异常。

import subprocess

try:
    ## Try to list a directory that does not exist
    ## check=True raises an exception if the command fails
    subprocess.run(["ls", "/nonexistent"], check=True, capture_output=True)

except subprocess.CalledProcessError as e:
    print("An error occurred!")
    print(f"Return code: {e.returncode}")
    ## We can also print the error message generated by the command (stderr)
    print(f"Error message: {e.stderr.decode('utf-8')}")

保存并退出。

运行脚本:

python3 handle_error.py

你应该会看到表示错误已被捕获的输出:

An error occurred!
Return code: 2
Error message: ls: cannot access '/nonexistent': No such file or directory

如果不使用 try-except 代码块,程序会因为冗长的回溯信息而崩溃。使用这种方式,脚本就能检测失败并决定下一步操作,例如记录错误、重试或正常退出。

总结

在本实验中,你学习了使用 Python 的 subprocess 模块执行 Shell 命令的基础知识。你练习了:

  • 导入 subprocess 模块。
  • 使用 subprocess.run() 运行命令。
  • 使用 capture_output=True 捕获标准输出。
  • 将字节输出解码为可读字符串。
  • 使用 check=True 和 try-except 代码块处理命令失败。

这些技能对于使用 Python 构建自动化脚本、部署工具和系统管理实用程序非常重要。