在 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 | "http://duckduckgo.com/", "-o", "page.html", "--silent") sh.curl( |
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
环境中,以便可以正确地运行.