Python 中有一个非常有意思的包 sh, 它是一个将系统程序动态映射到 Python 函数的一个 wrapper. 得益于 Python 的强大和灵活, 它可以让你在 Python 中方便地运行 shell 命令.


目录

  1. 开始使用 sh
    1. 安装 sh
    2. 简单使用
  2. 进阶
    1. A. 传递参数
    2. B. 返回值处理
    3. C. 输出重定向
      1. 1. 重定向到文件
      2. 2. 文件对象
      3. 3. 回调函数
    4. D. 异步执行
      1. 1. 增量迭代.
      2. 2. 后台进程
      3. 3. 输出回调
      4. 4. 交互式回调
      5. 5. 完成回调
    5. E. Baking
    6. F. 管道
      1. 基本操作
      2. 进阶操作
    7. G. 子命令
      1. sudo
    8. H. 默认参数
    9. I. 环境变量
    10. J. 输入
    11. K. With 环境

开始使用 sh

sh是一个非常成熟的子进程接口, 可以让你在 Python 中就像调用函数一样调用任何程序, 比 subproces.Popen 调用更优雅, 也能更好地让你捕获和分析输出结果.

安装 sh

sh不是内置的模块, 需要通过 pipeasy_install 来安装

1
2
3
$ sudo pip install sh
### or
$ sudo easy_install sh

简单使用

使用 sh 非常简单, 下面举几个例子

1
2
3
4
5
6
7
8
>>> import sh
>>> sh.ls("-l","-a")
...
>>> from sh import ip,git
>>> ip.address()
...
>>> git("status")
...

进阶

下面分几部分简单介绍 sh 的几个用法.

A. 传递参数

传递给命令的参数必须是单个的字符串, 可以传递多个参数, 或者用 split() 函数将其分成字符串数组, 传递过去

1
2
3
4
5
>>> 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 长短两种格式参数.

1
2
3
>>> sh.curl("http://duckduckgo.com/", "-o", "page.html", "--silent")
### **kwargs
>>> sh.curl("http://duckduckgo.com/", o="page.html", silent=True)

B. 返回值处理

C. 输出重定向

sh 可以将一个进程或所有进程的 STDOUTSTDERR 重定向到不同的目标, 通过使用 _out_err 关键字来指定.

1. 重定向到文件

如果 _out_err 为一个字符串, 通常被认为是一个文件名, 文件以 二进制写 (wb) 的方式被打开.

1
2
>>> import sh
>>> sh.ifconfig(_out = "/tmp/ifconfig.txt")

2. 文件对象

我们也可以使用支持 .write(data) 的任何对象作为重定向目标, 例如 io.StringIO:

1
2
3
4
5
>>> import sh
>>> from io import StringIO as io
>>> buf = io()
>>> sh.ifconfig(_out = buf)
>>> print(buf.getvalue())

3. 回调函数

一个回调函数也可以用作重定向目标, 其至少必须能接受进程的输出数据块.

1
fn(data)

D. 异步执行

sh 提供了几种用于异步执行命令的方式.

1. 增量迭代.

我们可以通过 _iter来创建异步命令进程, 最常见的例子就是 tail -f, 可以用在 loop 环境中:

1
2
3
>>> 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 &一样.

1
2
3
4
5
6
7
>>> 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 为例,

1
2
3
4
5
>>> 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 关键字传递给命令.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> 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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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 会把内层的输出传递给外层的命令:

1
2
3
>>> import sh
>>> print(sh.sort(sh.du(".", "-rn")))
>>> print(sh.wc(sh.ls("/etc", "-l"), "-l"))

为了减少出错, 可以结合上面的 baking 来食用.

进阶操作

一般地, 通过管道连接的命令会依次执行, 在大多数情况下是没有问题的. 但是对于持续输出的命令或需要并行化的地方, 这就不适用了. 比如下面的例子:

1
2
>>> for l in sh.tr(sh.tail("-f", 'test.log'), "[:upper:]", "[:lower:]", _iter = True):
print(l)

由于 tail -f不会结束, tr命令也就无法执行. 这里我们需要在tail 接收到输入时便将其传递给tr, 这可以通过 _piped=True 来实现.

1
2
>>> 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 中可以将子命令当作参数, 也可以想调用函数一样使用.

1
2
3
4
5
>>> 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.

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
2
3
4
5
>>> import sh
>>> with sh.crontrib.sudo:
print(sh.ls("/root"))
#### or via subcommand
>>>> print(sh.crontrib.sudo.ls("/root"))

然后它会要求你输入密码:

1
2
[sudo] password for your_name: *******
your_root_file

3. sh.sudo with password passed by
我们可以通过将密码传递给 sh.sudo的方式来使用, 结合 baking 更加方便, 不过不推荐这种方式.

1
2
3
4
>>> import sh
>>> my_password='password\n'
>>> my_sudo = sh.sudo.bake("-S", _in=my_password)
>>> print(my_sudo.ls("/root"))

4. _fg=True
这种方式无法捕获程序输出, 只会输出到终端.

1
2
>>> import sh
>>> sh.sudo.ls("/root", _fg=True)

H. 默认参数

许多时候, 你想覆盖掉所有命令的默认参数. 比如你想将所有的输出都聚合到 一个 io.StringIO 缓冲区 buf 里, 你可以在执行每一个命令的时候显示地传递 _out=buf, 但是太不优雅了. 我们可以对 sh 设定默认的参数并赋值给一个 execution context, 甚至可以从中 导入 sh 中的命令

1
2
3
4
5
6
7
8
9
>>> 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 关键字可以以字典类型传递环境变量:

1
2
>>> import sh
>>> sh.google_chrome(_env={"SOCKS_SERVER":"localhost:12345"})

需要注意的是, _env 会完全替换进程的环境变量. 只有 _env 里的键值对才会被使用. 如果要在现有的变量中加入新的环境变量, 可以通过 os.environ.copy() 命令来实现.

1
2
3
4
5
>>> 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 关键字传递给命令.

1
2
3
4
5
>>> 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:

1
2
>>> with sh.contrib.sudo(_with=True):
print(ls("/root"))

_with=True 关键字告诉命令它正处于 with 环境中, 以便可以正确地运行.