05月11, 2022

优雅的调用 Shell

Python 经常被称为胶水语言,正是因为它可以非常轻松的与其他语言开发的工具交互,形成工具链,共同组成强大的工具。

Python 调用其他可执行文件,可执行文件会作为子进程运行,下面是一些常见方法:

  • os.system,os.exec,这种方式相当于在 shell 中执行命令,Python 只能获得一个整数的状态码,很难直接与子进程交互,Python 只能干看着子进程运行,跑哪算哪。
  • 另外就是 os.popen,可以获得子进程的标准输入、输出,比较方便的与子进程交互。
  • 现代 Python 提供了 subprocess 模块,进一步简化了与子进程的交互,这也是社区推荐的方式。

下面是一个简单的例子:

>>> from subprocess import Popen, PIPE
>>> proc = Popen(["ls", "/home"], stdout = PIPE, text = True)
>>> print(proc.stdout.read())
1.txt

可不可以向子进程输入数据呢?当然也是可以的。

>>> from subprocess import Popen, PIPE
>>> proc = Popen(["cat"], text = True, stdin = PIPE, stdout = PIPE)   # cat 会回显所有输入
>>> proc.stdin.write("hello") # 向标准输入写数据
5
>>> proc.stdin.flush()
>>> out, ret = proc.communicate() # 获得输出
>>> out
'hello'

Pythonic way

上面的方法没有问题,而且其实也蛮好用的,直到我发现了一个更符合 Python 风格的模块,名字也很直接,sh。

经过简单的安装:

pip install sh

就可以使用了,前面第一个例子,会变成这样:

>>> import sh
>>> print(sh.ls("/home"))
1.txt

这才有那个味,优雅!

有趣的现象

一番使用下来,我发现 sh 支持大量的 shell 命令,这让我好奇作者的耐心,竟然将所有的 shell 命令都封装进去。为了探明 sh 的实现,我写了一个多情的脚本调戏它:

#!/bin/bash
echo "hello"

将它保存为名为 jiangchaun 的脚本, 并赋予可执行权限,再复制到 /bin/ 目录下(我用的是 Linux 系统),就能在任何位置用 shell 看到我的问候:

# jiangchuan
hello

我为系统增加了一个之前不存在的命令,sh 能否正常调用 jiangchuan 命令呢?

>>> import sh
>>> print(sh.jiangchuan())
hello

它可以的!显然,sh 并不是将所有命令都封装进去,而是使用了一些巧妙的魔法。

亲自优雅

如果检查 sh 的成员,会发现其中不包含比如 sh.ls 或者 sh.jiangchuan 这样的属性,实际上只要实现对象的 __getattr__ 方法,就可以按照自己的需要响应那些不存在的属性查询。

>>> class Cmder:
  def __init__(self):
    self.value = "存在的属性"

  def __getattr__(self, attr):
    print("查找属性:" + attr)
    return "you got me"

>>> cmder = Cmder()
>>> print(cmder.a)
查找属性:a
you got me
>>> print(cmder.b)
查找属性:b
you got me
>>> print(cmder.value)
存在的属性

可以看到,当试图访问不存在的属性:a, b 时,属性查询被 __getattr__ 方法处理,对于 Cmder 对象,我们知道用户试图查找的属性名称。Cmder 拥有一个属性:value,当试图查找存在的属性,则不会调用 __getattr__ 方法,直接返回 value 的值。

这正是 sh 模块采用的策略,访问任何不存在属性,sh 都会将属性名当作命令名称,尝试通过 subprocess 调用它,并获得输出。

我也实现了一个简单 Cmder,功能与 sh 类似,可以指定一个目录,以 cmder.xxx() 的形式调用该目录下的可执行文件。

总结

巧妙利用 Python 的魔术方法,可以将一切都变成 “面向对象” 的,非常符合使用直觉,这正是 Python 的魅力所在。

本文链接:http://www.thinkinpython.com/post/gracefully_invoke_shell.html

-- EOF --