在 Python 中使用 SH 模块执行 SHELL 命令
Python 中有一个非常有意思的包 sh, 它是一个将系统程序动态映射到Python函数的一个wrapper. 得益于Python的强大和灵活, 它可以让你在Python中方便地运行shell命令.
Table of Contents
开始使用 sh
sh是一个非常成熟的子进程接口, 可以让你在Python中就像调用函数一样调用任何程序, 比 subproces.Popen 调用更优雅,也能更好地让你捕获和分析输出结果.
安装 sh
sh不是内置的模块,需要通过 pip或 easy_install 来安装
1 | $ sudo pip install sh |
简单使用
使用 sh 非常简单, 下面举几个例子
1 | import sh |
进阶
下面分几部分简单介绍 sh 的几个用法.
A. 传递参数
传递给命令的参数必须是单个的字符串,可以传递多个参数, 或者用 split() 函数将其分成字符串数组,传递过去
1 | from sh import tar |
sh可以通过关键字参数 **kwargs 的形式传递参数, 但这种形式不保证参数顺序,一般使用上面形式就行. 这种形式支持 -a 和 --arg 长短两种格式参数.
1 | sh.curl("http://duckduckgo.com/", "-o", "page.html", "--silent") |
B. 返回值处理
C. 输出重定向
sh 可以将一个进程或所有进程的 STDOUT 和 STDERR 重定向到不同的目标, 通过使用 _out 和 _err 关键字来指定.
1. 重定向到文件
如果_out 或 _err为一个字符串, 通常被认为是一个文件名, 文件以 二进制写(wb)的方式被打开.
1 | import sh |
2. 文件对象
我们也可以使用支持 .write(data) 的任何对象作为重定向目标, 例如 io.StringIO:
1 | import sh |
3. 回调函数
一个回调函数也可以用作重定向目标, 其至少必须能接受进程的输出数据块.
1 | fn(data) |
D. 异步执行
sh 提供了几种用于异步执行命令的方式.
1. 增量迭代.
我们可以通过 _iter来创建异步命令进程,最常见的例子就是 tail -f, 可以用在 loop 环境中:
1 | from sh import tail |
2. 后台进程
我们可以将一些耗时长但又不需要立即获得结果的命令通过 _bg=True 放在后台运行, 就像 bash 中 some_command &一样.
1 | from sh import sleep |
我们需要在适当的时候执行 running_command.wait() 来等待后台运行的程序正常结束.
3. 输出回调
与 _bg=True 结合, sh 可以调用回调函数重定向 out 和/或 _err, 回调函数会在命令输出一行或块时被调用. 以 tail -f 为例,
1 | from sh import tail |
4. 交互式回调
5. 完成回调
完成回调( done callback) 是在进程正常结束(成功/出错) 或者接收到信号时,被执行, 通过 _done 关键字传递给命令.
1 | import sh |
E. Baking
sh 支持将参数跟命令 烘培(baking)在一起, 类似于bash中的别名(alias).
1 | from sh import ls,ssh |
F. 管道
基本操作
Bash风格的管道可以通过函数嵌套来实现. 简单来说,就是把一个命令当作另一个命令的参数来使用, sh 会把内层的输出传递给外层的命令:
1 | import sh |
为了减少出错,可以结合上面的 baking 来食用.
进阶操作
一般地, 通过管道连接的命令会依次执行, 在大多数情况下是没有问题的. 但是对于持续输出的命令或需要并行化的地方, 这就不适用了.比如下面的例子:
1 | for l in sh.tr(sh.tail("-f", 'test.log'), "[:upper:]", "[:lower:]", _iter = True): |
由于 tail -f不会结束, tr命令也就无法执行. 这里我们需要在tail 接收到输入时便将其传递给tr, 这可以通过 _piped=True 来实现.
1 | for l in sh.tr(sh.tail("-f", 'test.log', _piped=True), "[:upper:]", "[:lower:]", _iter = True): |
这样就告诉 tail -f 它正处于管道中,应该将其输出一行一行地发动给tr. 缺省情况, _piped=True 会发送 STDOUT, 如果想发送错误,可以使用 _piped="err".
G. 子命令
许多命令都会有子命令如git, svn, ip, sudo等. sh 中可以将子命令当作参数, 也可以想调用函数一样使用.
1 | from sh import git, sudo |
sh 中的子命令主要是语法糖, 让调用命令更优雅一些.
sudo
sudo 的情况比较特殊, sh中有三种调用sudo的方法.
1. sh.sudo with /etc/sudoers NOPASSWD
通过设定 用户免输入密码, 可以直接使用sh.sudo.
1 | $ sudo visudo |
在最后添加或修改权限
1 | your_name ALL = (root) NOPASSWD: /path/to/your/program |
这是说你可以在 所有(ALL)的主机上可以且只能以root 免密运行 /path/to/your/program, 如果需要运行的程序很多, 可以设置所有程序都免密执行.
1 | your_name ALL = (root) NOPASSWD: ALL |
2. sh.crontrib.sudo
因为sudo 使用频率非常高, sh特别添加了一个 普通版本的 sudo 使得 sudo更加好用. 它只是对sh.sudo做了简单的包装, 但是 bake 了一些特别的关键字参数使得其更加方便.
1 | import sh |
然后它会要求你输入密码:
1 | [sudo] password for your_name: ******* |
3. sh.sudo with password passed by
我们可以通过将密码传递给 sh.sudo的方式来使用,结合baking更加方便, 不过不推荐这种方式.
1 | import sh |
4. _fg=True
这种方式无法捕获程序输出, 只会输出到终端.
1 | import sh |
H. 默认参数
许多时候,你想覆盖掉所有命令的默认参数. 比如你想将所有的输出都聚合到 一个 io.StringIO 缓冲区 buf 里, 你可以在执行每一个命令的时候显示地传递 _out=buf, 但是太不优雅了. 我们可以对 sh 设定默认的参数并赋值给一个 execution context, 甚至可以从中 导入 sh 中的命令
1 | import sh |
I. 环境变量
_env 关键字可以以字典类型传递环境变量:
1 | import sh |
需要注意的是, _env 会完全替换进程的环境变量.只有 _env 里的键值对才会被使用. 如果要在现有的变量中加入新的环境变量, 可以通过 os.environ.copy() 命令来实现.
1 | import sh |
J. 输入
STDIN 可以通过 _in 关键字传递给命令.
1 | import sh, sys |
可以用 文件对象, queue.Queue, 或者其他任何可以迭代的对象作为参数,如上面所示.
K. With 环境
命令可以运行在 Python 的 with 环境中, 常用的命令可能是 sudo 或者 fakeroot:
1 | with sh.contrib.sudo(_with=True): |
_with=True 关键字告诉命令它正处于 with 环境中,以便可以正确地运行.