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


