python之sh,更加人性化的subp

2023-01-31 05:01:28 python sh 人性化

python之sh快速入门

 

前言:虽然Python有足够多的库来干足够多的事,但是一些基于linux脚本类的小事,还是会觉得用shell脚本要更方便,再但是我就是想用python呀,怎么完美的糅合两者呢?我想sh是一个很好的选择,非常优雅。


注:还是那句话,自己手工翻译的,有点粗糙,没有校正,估计也不会校正了,权当练手的

参考:https://amoffat.GitHub.io/sh/

 

 

sh是个很酷的模块,当前似乎只支持Linux,OSX。有点requests之于URLlib2的赶脚

 

sh(前身pbs)是基于python完全的subprocess接口,允许像调用函数一样调用的所有程序。

 

from sh import ifconfig

print(ifconfig("wlan0"))

wlan0   Link encap:Ethernet  HWaddr 00:00:00:00:00:00

        inet addr:192.168.1.100  Bcast:192.168.1.255  Mask:255.255.255.0

        inet6 addr: ffff::ffff:ffff:ffff:fff/64 Scope:Link

        UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1

        RX packets:0 errors:0 dropped:0 overruns:0 frame:0

        TX packets:0 errors:0 dropped:0 overruns:0 carrier:0

        collisions:0 txqueuelen:1000

        RX bytes:0 (0 GB)  TX bytes:0 (0 GB)

 

其他的例子:

# checkout 

git.checkout("master")

 

# ls当前目录

print(ls("-l"))

 

# 获取最长的文件文件名

longest_line = wc(__file__, "-L")

注:其中的git,ls,wc命令得from sh import git,ls,wc导入

 

值得注意的是,这些都不是python自有的函数,它们都是一些基于你自身系统,通过$PATH解析的可执行命令,非常像bash操作,所有在你系统的程序可以非常简单的在python里面调用。所以无论调用ifconfig,wc,还是ls的前提是系统存在这个可执行命令。

 

安装

 

pip install sh

 

github:Http://github.com/amoffat/sh

 

教程

Tutorial 1: Tailing a real-time log file

Tutorial 2: Entering an ssh passWord

 

基本特点

命令执行

命令的调用就像函数一样,可以在sh的namespace里执行,或者通过sh直接导入


import sh

print(sh.ls("/"))

 

# 同上

from sh import ls

print(ls("/"))

 

如果可执行命令的文件名有横杠,比如/usr/bin/Google-chrom,我们需要用下划线代替

import sh

sh.google_chrome("http://google.com")

 

注意:如果可执行命令的文件名存在一些其他的符号,比如点.,你或许可以通过sh的command将命令的文件名或者绝对路径封装起来

import sh

run = sh.Command("/home/amoffat/run.sh") # Absolute path

run()lscmd = sh.Command("ls")  # Absolute path not needed

lscm

 

多重参数

 

这些接收多个参数的命令,这些参数的调用需要使用单独的字符串指定每个参数,而不是一个字符串代表所有的参数。或许有人会认为下面的命令会在*nix shell里可用,但是并非如此

from sh import tar

tar("cvf /tmp/test.tar /my/home/directory")

 

你将会得到下面的错误信息

RAN: '/bin/tar cvf /tmp/test.tar /my/home/directory'

 

STDOUT:

 

STDERR:

/bin/tar: Old option 'f' requires an argument.

Try '/bin/tar --help' or '/bin/tar --usage' for more infORMation.

 

正确的方式是在你传入的时候就将其分隔成单个字符,Shell(bash)会帮你这么做。他在这些参数传入程序之前会将”tar  cvf /tmp/test.tar /my/home/directory”分成四个字符串:”tar”,”cvf”,”/tmp/test.tar”,”/my/home/direcory”。所以在使用sh的时候你必须手动完成这些。

 

from sh import tar

tar("cvf", "/tmp/test.tar", "/my/home/directory/")

 

sh的command参数封装

 

像上面一样,传给sh.Commnad的参数必须是分隔的,例如,下面的例子是不能生效的。

lscmd = sh.Command("/bin/ls -l")

tarcmd = sh.Command("/bin/tar cvf /tmp/test.tar /my/home/directory/")

 

你将在运行的时候得到CommnadNotFound(path)异常,即使这些命令的指定路径完全正确。正确的方式如下:

1,只使用二进制文件生成Command对象

2,传入参数时调用如下

lscmd = sh.Command("/bin/ls")

lscmd("-l")

tarcmd = sh.Command("/bin/tar")

tarcmd("cvf", "/tmp/test.tar", "/my/home/directory/")

 

关键字参数

Command支持短格式-a以及长格式--arg作为关键字参数:

curl http://duckduckgo.com/ -o page.html --silent可以写成以下格式

curl("http://duckduckgo.com/", o="page.html", silent=True)

#或者

curl("http://duckduckgo.com/", "-o", "page.html", "--silent")

 

 

 

后台处理

默认情况下,每一个命令return之前运行并完成它的进程。

如果你有一个运行时间较长的命令,你能够将它通过_bg=True指定关键字参数放入后台

#!/usr/bin/env python

#coding:utf-8

 

from sh import sleep

                                                                                           

p = sleep(3,_bg=True)

print "直接打印"

p.wait()

print("进程三秒后")

 

 

管道

类Bash管道是在执行构造函数。将一个命令作为另一个名的输入,sh将会创造一个管道在两者之间。

# 给最大的文件排序

print(sort(du(glob("*"), "-sb"), "-rn"))

 

# 打印/etc目录下的文件数

print(wc(ls("/etc", "-1"), "-l"))

 

 

默认情况下,任何命令的管道是等另一个命令执行完成后。但是这个动作能够通过_piped关键字参数改变。这个参数告诉它不用等另一个命名完成之后才传入结果,而是增量的。更加高级的管道例子。

 

 

重定向

sh能够重定向教程的标准输出以及错误输出数据流到一个文件或者类文件对象。这些通过_out和_err关键字参数指定。你可以传入一个文件名或者文件对象作为参数值。当已存在的文件的文件名传入,文件的内容会被覆盖。

ls(_out="files.list")

ls("nonexistent", _err="error.txt")

 

你可以重定向到一个函数,参考STDOUT/ERR callbacks.

 

标准输入处理

标准输入处理可以通过_in关键字参数直接指定。

print(cat(_in="test")) # 打印 "test"

 

任何的命令的标准输入可以像如下使用。

print(tr("[:lower:]", "[:upper:]", _in="sh is awesome")) # SH IS AWESOME

 

标注输入并不局限于字符串,你可以使用文件对象,队列,或者任何可以迭代的(列表,集合。字典等):

stdin = ["sh", "is", "awesome"]

out = tr("[:lower:]", "[:upper:]", _in=stdin)

 

子命令

许多程序拥有自己子命令集合,想git(branch,checkout),svn(update,status),以及sudo(任何接在sudo后面的命令都能认为是子命令)。sh的子命令通过属性存取处理。

from sh import git, sudo

 

# "git branch -v"

print(git.branch("-v"))

print(git("branch", "-v")) # 同上一样

 

#"sudo /bin/ls /root"

print(sudo.ls("/root"))

print(sudo("/bin/ls", "/root")) # 同上一样

 

 

注意:如果你使用sudo,执行的用户必须是没有设置密码的,不然sudo会被挂起。

 

返回值

正常情况下教程在退出的时候返回返回值,这些能通过Command的exit_code获得。

output = ls("/")

print(output.exit_code) #应该是0

 

 

如果教程伴随着错误终止或者非0的返回值,将会动态产生一个异常。这些能让你抓取指定的返回值或者通过ErrorReturnCode类抓取所有返回值。

 

try: 

    print(ls("/some/non-existant/folder"))

except ErrorReturnCode_2:

    print("folder doesn't exist!")

    create_the_folder()except ErrorReturnCode:

    print("unknown error")

    exit(1)

 

注意:不会抛出ErrorReturnCode信号。命令将会向成功一样返回,但是它的exit_code值会被设置带-singal_num。因此,举例来说,如果一个命令一个命令被sighup,它的返回值将会是-1

 

一些程序返回一些奇怪的返回值,即使它是成功的。如果你知道这些程序的返回值,你将不需要对其进行操作,你能通过_ok_code关键字参数指定:

import sh

sh.weird_program(_ok_code=[0,3,5])

 

着意味着如果进程的返回值是0,3或者5都不会抛出异常。

 

注意:如果你使用_ok_code,你必须指定所有被封为是正常退出的返回值。

 

全局匹配符

全局匹配符不会再你的参数中生效,如下。

import shsh.ls("*.py")

 

你会得到cannot access '\*.py': No such file or directory的结果,这是因为*py需要全局匹配,

import sh

sh.ls(sh.glob("*.py"))

 

注意:不要使用python的glob.glob函数。

 

 

进阶特性

 

Baking

sh有baking参数到commds的特点(类似于alias命令),这跟stdlib.functools.partial封装有点类似,比如:

from sh import ls

ls = ls.bake("-la")

print(ls) # "/usr/bin/ls -la"

 

# "ls -la /"

print(ls("/"))

 

ls命令将会绑定“-la”参数,当你跟子命令一起用的时候非常有趣。

from sh import ssh

# 在服务器上执行whoami

iam1 = ssh("myserver.com", "-p 1393", "whoami")

 

#其他方式

myserver = ssh.bake("myserver.com", p=1393)

print(myserver) # "/usr/bin/ssh myserver.com -p 1393"

 

 

# "/usr/bin/ssh myserver.com -p 1393 whoami"

iam2 = myserver.whoami()

assert(iam1 == iam2) # True!

 

现在调用“myserver”将会表述为一个被baked的ssh命令。你能在服务器上调用任何事。

# "/usr/bin/ssh myserver.com -p 1393 tail /var/log/dumb_daemon.log -n 100"

print(myserver.tail("/var/log/dumb_daemon.log", n=100))

 

with环境

命令能在with环境中运行,最常见的命令大概就是su和fakeroot了

with sudo:

    print(ls("/root"))

 

如果在with环境的同时传入参数运行,比如,指定-p提示符,你可以使用_with关键字参数指定。这能让命令知道它是运行在with环境中。

with sudo(k=True, _with=True):

    print(ls("/root"))

 

迭代输出内容

 

你能够通过指定_iter关键字参数迭代一个长期运行的命令,它将会创建你能循环的一个迭代器(技术上来说,是一个生成器)

from sh import tail

# runs forever

for line in tail("-f", "/var/log/some_log_file.log", _iter=True):

    print(line)

 

默认情况下,_iter迭代标准输出,但是你也能手动更改,比如指定“err”或者“out”。另外,输出是以行缓存的,但是你也能手动更改buffer size。

 

注意:如果你需要一个非阻塞的迭代器,使用_iter_noblock。如果当前迭代器会堵塞,errno.EWOULDBLOCK将会返回,另外你会接受到一大块正常的输出。

 

标准输出/错误回调函数

 

sh能够通过回调函数增量的处理输出。这非常像重定向:通过传入参数_out 或者 _err(或者两者)

from sh import tail

def process_output(line):

    print(line)

p = tail("-f", "/var/log/some_log_file.log", _out=process_output)p.wait()

 

为了控制数据行或者数据块的调用,请参考Buffer sizes。退出回调回调函数只要简单的返回True即可,这会告诉命令不再调用你的回调函数。

 

注意:返回True并不杀死教程

 

交互式回调函数

 

每一个通过sh启动的命令有一个内置的标准输入队列,用于回调函数。

 

def interact(line, stdin):

    if line == "What... is the air-speed velocity of an unladen swallow?":

        stdin.put("What do you mean? An African or European swallow?")

 

    elif line == "Huh? I... I don't know that....AAAAGHHHHHH":

        cross_bridge()

        return True

 

    else:

        stdin.put("I don't know....AAGGHHHHH")

        return True

sh.bridgekeeper(_out=interact).wait()

 

 

你也能杀死或者终止你的进程通过你的回调函数

def process_output(line, stdin, process):

    print(line)

    if "ERROR" in line:

        process.kill()

        return True

p = tail("-f", "/var/log/some_log_file.log", _out=process_output)p.wait()

 

上面的代码将会在运行的时候,实时打印some_log_file.log文件里的后十行,直到在打印的内容行里出现“ERROR”字符,并杀死进程以及终止脚本。

 

注意:你也能使用.terminate()发送SIGTERM信号,或者使用.signal(sig)去发送普通信号。

 

Buffer Size

 

当你开始使用迭代器,高级管道或者回调函数是缓存大小非常重要。Tutorial 2: Entering an SSH password提供一个不错的例子,说明为什么需要不同的buffering模式。buffer size控制标准输入的读入以及标准输出/错误的写入。考虑如下:

for chunk in tr("[:lower:]", "[:upper:]", _in="testing", _iter=True):

    print(chunk)

 

Because now we set STDOUT to also be unbuffered with _out_bufsize=0 the result is “T”, “E”, “S”, “T”, “I”, “N”, “G”, as expected.

There are 2 bufsize special keyword arguments: _in_bufsize and _out_bufsize. They may be set to the following values:

0

Unbuffered. For STDIN, strings and file objects will be read character-by-character, while Queues, callables, and iterables will be read item by item.

1

Line buffered. For STDIN, data will be passed into the process line-by-line. For STDOUT/ERR, data will be output line-by-line. If any data is remaining in the STDOUT or STDIN buffers after all the lines have been consumed, it is also consumed/flushed.

N

Buffered by N characters. For STDIN, data will be passed into the process <=N characters at a time. For STDOUT/ERR, data will be output <=N characters at a time. If any data is remaining in the STDOUT or STDIN buffers after all the lines have been consumed, it is also consumed/flushed.

Advanced piping

By default, all piped commands execute sequentially. What this means is that the inner command executes first, then sends its data to the outer command:

print(wc(ls("/etc", "-1"), "-l"))

In the above example, ls executes, gathers its output, then sends that output to wc. This is fine for simple commands, but for commands where you need parallelism, this isn’t good enough. Take the following example:

for line in tr(tail("-f", "test.log"), "[:upper:]", "[:lower:]", _iter=True):

    print(line)

This won’t work because the tail -f command never finishes. What you need is for tail to send its output to tr as it receives it. This is where the _pipedspecial keyword argument comes in handy:

for line in tr(tail("-f", "test.log", _piped=True), "[:upper:]", "[:lower:]", _iter=True):

    print(line)

This works by telling tail -f that it is being used in a pipeline, and that it should send its output line-by-line to tr. By default, _piped sends stdout, but you can easily make it send stderr instead by using _piped="err"

Environments

The special keyword argument _env allows you to pass a dictionary of environement variables and their corresponding values:

import shsh.google_chrome(_env={"SOCKS_SERVER": "localhost:1234"})

Note

 

_env replaces your process’s environment completely. Only the key-value pairs in _env will be used for its environment. If you want to add new environment variables for a process in addition to your existing environment, try something like this:

import osimport sh

new_env = os.environ.copy()new_env["SOCKS_SERVER"] = "localhost:1234"

sh.google_chrome(_env=new_env)

 

 


相关文章