python制作命令行工具_用 Plumbum 开发 Python 命令行工具

摘要:本文翻译自 Python Plumbum[1] 开源库的官方文档 Plumbum CLI[2] 部分,主要介绍如何使用 Plumbum CLI[2] 工具包来开发 Python 命令行应用程序,这是一个非常 Pythonic、容易使用、功能强大的工具包,非常值得广大 Python 程序员掌握并使用。

轻松执行程序的另一方面是轻松编写 CLI 程序。Python 脚本一般使用 optparse 或者最新的 argparse 及其衍生品来开发命令行工具,但是所有这些表现力有限,而且非常不直观(甚至不够 Pythonic)。Plumbum 的 CLI 工具包提供了一个程序化的方法来构建命令行应用程序,不需要创建一个解析器对象,然后填充一系列“选项”,该 CLI 工具包使用内省机制将这些原语转义成 Pythonic 结构。

总体来看,Plumbum CLI 应用程序是一个继承自 plumbum.cli.Application 的类。这些类定义了一个 main() 方法,并且可选地公开出方法和属性来作为命令行的选项。这些选项可能需要参数,而任何剩余的位置参数会根据 main 函数的声明来将其赋予 main 方法。一个简单的 CLI 应用程序看起来像如下这样:

fromplumbumimportcli

classMyApp(cli.Application):

verbose=cli.Flag(['v','verbose'],help='If given, I will be very talkative')

defmain(self,filename):

print('I will now read {0}'.format(filename))

ifself.verbose:

print('Yadda '*200)

if__name__=='__main__':

MyApp.run()

你可以运行该程序:

$ python example.py foo

I will now read foo

$ python example.py--help

example.py v1.0

Usage:example.py[SWITCHES]filename

Meta-switches:

-h,--helpPrintsthishelp messageandquits

--versionPrintsthe program's version and quits

Switches:

-v, --verbose              If given, I will be very talkative

到现在为止,你只看到了非常基本的使用。我们现在开始探索该库。

新版本 1.6.1: 你可以直接运行应用程序 MyApp(),不需要参数,也不需要调用 .main()。

应用程序

Application 类是你的应用程序的“容器”,该“容器”由一个你需要实现的main()方法和任何数量公开选项函数和属性。你的应用程序的入口是类方法 run,该方法实例化你的类、解析参数、调用所有的选项函数,然后使用给的位置参数来调用main()函数。为了从命令行运行你的应用程序,你所要做的是:

if__name__=='__main__':

MyApp.run()

除了 run() 和 main(),Application 类还公开了两个内置的选项函数:help()和 version(),分别用于显示帮助和程序的版本。默认情况下,--hep 和 -h 会调用 help(),--version 和 -v 会调用 version(),这些函数被调用后会显示相应的信息然后退出(没有处理任何其他选项)。

你可以通过定义类属性来自定义 help() 和 version() 显示的信息,比如 PROGNAME、 VERSION 和 DESCRIPTION。举例:

classMyApp(cli.Application):

PROGNAME='Foobar'

VERSION='7.3'

颜色

新版本 1.6

该库也支持终端字符颜色控制。你可以直接将 PROGNAME, VERSION 和 DESCRIPTION变为带颜色的字符串。如果你给 PROGNAME 设置了颜色,你会得到自定义的程序名字和颜色。使用方法字符串的颜色可以通过设置 COLOR_USAGE 来生效,不同选项组的颜色可以通过设置 COLOR_GROUPS 字典来生效。

举例如下:

classMyApp(cli.Application):

PROGNAME=colors.green

VERSION=colors.blue|'1.0.2'

COLOR_GROUPS={'Meta-switches':colors.bold&colors.yellow}

opts=cli.Flag('--ops',help=colors.magenta|'This is help')

SimpleColorCLI.py1.0.2

Usage:

SimpleColorCLI.py[SWITCHES]

Meta-switches

-h,--helpPrintsthishelp messageandquits

--help-allPrinthelp messages of all subcommandsandquit

-v,--versionPrintsthe program's version and quits

Switches

--ops              This is help

选项函数

switch 装饰器是该 CLI 开发工具包的“灵魂”,它会公开你的 CLI 应用程序的方法来作为 CLI 命令行选项,这些方法运行通过命令行来调用。我们测试下如下应用:

classMyApp(cli.Application):

_allow_root=False#provide adefault

@cli.switch('--log-to-file',str)

deflog_to_file(self,filename):

'''Sets the file into which logs will be emitted'''

logger.addHandler(FileHandle(filename))

@cli.switch(['-r','--root'])

defallow_as_root(self):

'''If given, allow running as root'''

self._allow_root=True

defmain(self):

ifos.geteuid()==0andnotself._allow_root:

raiseValueError('cannot run as root')

当程序运行时,选项函数通过相应的参数被调用。比如,$ ./myapp.py --log-to-file=/tmp/log 将被转化成调用 app.log_to_file('/tmp/log')。在选项函数被执行后,程序的控制权会被传递到 main 方法。注意

方法的文档字符串和参数名字会被用来渲染帮助信息,尽量保持你的代码 DRY[3]。

autoswitch 可以从函数名字中推断出选项的名称,举例如下:

@cli.autoswitch(str)

deflog_to_file(self,filename):

pass

这会将选项函数和 --log-to-file 绑定。

选项参数

如上面例子所示,选项函数可能没有参数(不包括 self)或者有一个参数。如果选项函数接受一个参数,必须指明该参数的类型。如果你不需要特殊的验证,只需传递 str,否则,您可能会传递任何类型(或实际上可调用的任何类型),该类型将接收一个字符串并将其转换为有意义的对象。如果转换是不可行的,那么会抛出 TypeError 或者 ValueError 异常。

举例:

classMyApp(cli.Application):

_port=8080

@cli.switch(['-p'],int)

defserver_port(self,port):

self._port=port

defmain(self):

print(self._port)

$./example.py-p17

17

$./example.py-p foo

Argumentof-p expected to be,not'foo':

ValueError('invalid literal for int() with base 10: 'foo'',)

工具包包含两个额外的“类型”(或者是是验证器):Range 和 Set。Range 指定一个最小值和最大值,限定一个整数在该范围内(闭区间)。Set 指定一组允许的值,并且期望参数匹配这些值中的一个。示例如下:

classMyApp(cli.Application):

_port=8080

_mode='TCP'

@cli.switch('-p',cli.Range(1024,65535))

defserver_port(self,port):

self._port=port

@cli.switch('-m',cli.Set('TCP','UDP',case_sensitive=False))

defserver_mode(self,mode):

self._mode=mode

defmain(self):

print(self._port,self._mode)

$./example.py-p17

Argumentof-p expected to be[1024..65535],not'17':

ValueError('Not in range [1024..65535]',)

$./example.py-m foo

Argumentof-m expected to beSet('udp','tcp'),not'foo':

ValueError('Expected one of ['UDP', 'TCP']',)注意 工具包中还有其他有用的验证器:ExistingFile(确保给定的参数是一个存在的文件),ExistingDirectory(确保给定的参数是一个存在的目录),NonexistentPath(确保给定的参数是一个不存在的路径)。所有这些将参数转换为本地路径[4]。

可重复的选项

很多时候,你需要在同一个命令行中多次指定某个选项。比如,在 gcc 中,你可能使用 -I参数来引入多个目录。默认情况下,选项只能指定一次,除非你给 switch 装饰器传递 list = True 参数。

classMyApp(cli.Application):

_dirs=[]

@cli.switch('-I',str,list=True)

definclude_dirs(self,dirs):

self._dirs=dirs

defmain(self):

print(self._dirs)

$./example.py-I/foo/bar-I/usr/include

['/foo/bar','/usr/include']注意 选项函数只被调用一次,它的参数将会变成一个列表。

强制的选项

如果某个选项是必须的,你可以给 switch 装饰器传递 mandatory = True 来实现。这样的话,如果用户不指定该选项,那么程序是无法运行的。

选项依赖

很多时候,一个选项的出现依赖另一个选项,比如,如果不给定 -y 选项,那么 -x 选项是无法给定的。这种限制可以通过给 switch 装饰器传递 requires 参数来实现,该参数是一个当前选项所依赖的选项名称列表。如果不指定某个选项所依赖的其他选项,那么用户是无法运行程序的。

classMyApp(cli.Application):

@cli.switch('--log-to-file',str)

deflog_to_file(self,filename):

logger.addHandler(logging.FileHandler(filename))

@cli.switch('--verbose',requires=['--log-to-file'])

defverbose(self):

logger.setLevel(logging.DEBUG)

$./example--verbose

Given--verbose,the following are missing['log-to-file']警告 选项函数的调用顺序和命令行指定的选项的顺序是一致的。目前不支持在程序运行时计算选项函数调用的拓扑顺序,但是将来会改进。

选项互斥

有些选项依赖其他选项,但是有些选项是和其他选项互斥的。比如,--verbose 和 --terse 同时存在是不合理的。为此,你可以给 switch 装饰器指定 excludes 列表来实现。

classMyApp(cli.Application):

@cli.switch('--log-to-file',str)

deflog_to_file(self,filename):

logger.addHandler(logging.FileHandler(filename))

@cli.switch('--verbose',requires=['--log-to-file'],excludes=['--terse'])

defverbose(self):

logger.setLevel(logging.DEBUG)

@cli.switch('--terse',requires=['--log-to-file'],excludes=['--verbose'])

defterse(self):

logger.setLevel(logging.WARNING)

$./example--log-to-file=log.txt--verbose--terse

Given--verbose,the following are invalid['--terse']

选项分组

如果你希望在帮助信息中将某些选项组合在一起,你可以给 switch 装饰器指定 group = 'Group Name', Group Name 可以是任意字符串。当显示帮助信息的时候,所有属于同一个组的选项会被聚合在一起。注意,分组不影响选项的处理,但是可以增强帮助信息的可读性。

选项属性

很多时候只需要将选项的参数存储到类的属性中,或者当某个属性给定后设置一个标志。为此,工具包提供了 SwitchAttr,这是一个数据描述符[5],用来存储参数。 该工具包还提供了两个额外的 SwitchAttr:Flag(如果选项给定后,会给其赋予默认值)和 CountOf(某个选项出现的次数)。

classMyApp(cli.Application):

log_file=cli.SwitchAttr('--log-file',str,default=None)

enable_logging=cli.Flag('--no-log',default=True)

verbosity_level=cli.CountOf('-v')

defmain(self):

print(self.log_file,self.enable_logging,self.verbosity_level)

$./example.py-v--log-file=log.txt-v--no-log-vvv

log.txtFalse5

环境变量

新版本 1.6

你可以使用 envname 参数将环境变量作为 SwitchAttr 的输入。举例如下:

classMyApp(cli.Application):

log_file=cli.SwitchAttr('--log-file',str,envname='MY_LOG_FILE')

defmain(self):

print(self.log_file)

$ MY_LOG_FILE=this.log./example.py

this.log

在命令行给定变量值会覆盖相同环境变量的值。

Main

一旦当所有命令行参数被处理后 ,main() 方法会获取程序的控制,并且可以有任意数量的位置参数,比如,在 cp -r /foo /bar 中, /foo 和 /bar 是位置参数。程序接受位置参数的数量依赖于 main() 函数的声明:如果 main 方法有 5 个参数,2 个是有默认值的,那么用户最少需要提供 3 个位置参数并且总数量不能多于 5 个。如果 main 方法的声明中使用的是可变参数(*args),那么位置参数的个数是没有限制的。

classMyApp(cli.Application):

defmain(self,src,dst,mode='normal'):

print(src,dst,mode)

$./example.py/foo/bar

/foo/bar normal

$./example.py/foo/bar spam

/foo/bar spam

$./example.py/foo

Expectedat least2positional arguments,got['/foo']

$./example.py/foo/bar spam bacon

Expectedat most3positional arguments,got['/foo','/bar','spam','bacon']注意 该方法的声明也用于生成帮助信息,例如:

Usage:[SWITCHES]src dst[mode='normal']

使用可变参数:

classMyApp(cli.Application):

defmain(self,src,dst,*eggs):

print(src,dst,eggs)

$./example.py a b c d

a b('c','d')

$./example.py--help

Usage:[SWITCHES]src dst eggs...

Meta-switches:

-h,--helpPrintsthishelp messageandquits

-v,--versionPrintsthe program's version and quits

位置验证

新版本 1.6

你可以使用 cli.positional 装饰器提供的验证器来验证位置参数。只需在装饰器中传递与 main 函数中的相匹配的验证器即可。例如:

classMyApp(cli.Application):

@cli.positional(cli.ExistingFile,cli.NonexistentPath)

defmain(self,infile,*outfiles):

'infile is a path, outfiles are a list of paths, proper errors are given'

如果你的程序只在 Python 3 中运行,你可以使用注解来指定验证器,例如:

classMyApp(cli.Application):

defmain(self,infile:cli.ExistingFile,*outfiles:cli.NonexistentPath):

'Identical to above MyApp'

如果 positional 装饰器存在,那么注解会被忽略。

子命令

新版本 1.1

随着 CLI 应用程序的扩展,功能变的越来越多,一个通常的做法是将其逻辑分成多个子应用(或者子命令)。一个典型的例子是版本控制系统,比如 git[6],git 是根命令,在这之下的子命令比如 commit 或者 push 是嵌套的。git 甚至支持命令别名,这运行用户自己创建一些子命令。Plumbum 写类似这样的程序是很轻松的。

在我们开始了解代码之前,先强调两件事情:◈ 在 Plumbum 中,每个子命令都是一个完整的 cli.Application 应用,你可以单独执行它,或者从所谓的根命令中分离出来。当应用程序单独执行是,它的父属性是 None,当以子命令运行时,它的父属性指向父应用程序。同样,当父应用使用子命令执行时,它的内嵌命令被设置成内嵌应用。◈ 每个子命令只负责它自己的选项参数(直到下一个子命令)。这允许应用在内嵌应用调用之前来处理它自己的选项和位置参数。例如 git --foo=bar spam push origin --tags:根应用 git 负责选项 --foo 和位置选项 spam ,内嵌应用 push 负责在它之后的参数。从理论上讲,你可以将多个子应用程序嵌套到另一个应用程序中,但在实践中,通常嵌套层级只有一层。

这是一个模仿版本控制系统的例子 geet。我们有一个根应用 Geet ,它有两个子命令 GeetCommit 和 GeetPush:这两个子命令通过 subcommand 装饰器来将其附加到根应用。

classGeet(cli.Application):

'''The l33t version control'''

VERSION='1.7.2'

defmain(self,*args):

ifargs:

print('Unknown command {0!r}'.format(args[0]))

return1#errorexitcode

ifnotself.nested_command:#will be``None``ifnosub-command follows

print('No command given')

return1#errorexitcode

@Geet.subcommand('commit')#attach'geet commit'

classGeetCommit(cli.Application):

'''creates a new commit in the current branch'''

auto_add=cli.Flag('-a',help='automatically add changed files')

message=cli.SwitchAttr('-m',str,mandatory=True,help='sets the commit message')

defmain(self):

print('doing the commit...')

@Geet.subcommand('push')#attach'geet push'

classGeetPush(cli.Application):

'''pushes the current local branch to the remote one'''

defmain(self,remote,branch=None):

print('doing the push...')

if__name__=='__main__':

Geet.run()注意◈ 由于 GeetCommit 也是一个 cli.Application,因此你可以直接调用 GeetCommit.run() (这在应用的上下文是合理的)◈ 你也可以不用装饰器而使用 subcommand 方法来附加子命令:Geet.subcommand('push', GeetPush)

以下是运行该应用程序的示例:

$ python geet.py--help

geet v1.7.2

Thel33t version control

Usage:geet.py[SWITCHES][SUBCOMMAND[SWITCHES]]args...

Meta-switches:

-h,--helpPrintsthishelp messageandquits

-v,--versionPrintsthe program's version and quits

Subcommands:

commit                     creates a new commit in the current branch; see

'geet commit--help' for more info

push                       pushes the current local branch to the remote

one; see 'geet push--help' for more info

$ python geet.py commit --help

geet commit v1.7.2

creates a new commit in the current branch

Usage: geet commit [SWITCHES]

Meta-switches:

-h, --help                 Prints this help message and quits

-v, --version              Prints the program's versionandquits

Switches:

-a                         automatically add changed files

-m VALUE:str               sets the commit message;required

$ python geet.py commit-m'foo'

doing the commit...

配置解析器

应用程序的另一个常见的功能是配置文件解析器,解析后台 INI 配置文件:Config (或者 ConfigINI)。使用示例:

fromplumbumimportcli

withcli.Config('~/.myapp_rc')asconf:

one=conf.get('one','1')

two=conf.get('two','2')

如果配置文件不存在,那么将会以当前的 key 和默认的 value 来创建一个配置文件,在调用 .get 方法时会得到默认值,当上下文管理器存在时,文件会被创建。如果配置文件存在,那么该文件将会被读取并且没有任何改变。你也可以使用 [] 语法来强制设置一个值或者当变量不存在时获取到一个 ValueError。如果你想避免上下文管理器,你也可以使用 .read和 .write。

ini 解析器默认使用 [DEFAULT] 段,就像 Python 的 ConfigParser。如果你想使用一个不同的段,只需要在 key 中通过 . 将段和标题分隔开。比如 conf['section.item'] 会将 item 放置在 [section] 下。所有存储在 ConfigINI 中的条目会被转化成 str,str 是经常返回的。

终端实用程序

在 plumbum.cli.terminal 中有多个终端实用程序,用来帮助制作终端应用程序。

get_terminal_size(default=(80,25)) 允许跨平台访问终端屏幕大小,返回值是一个元组 (width, height)。还有几个方法可以用来询问用户输入,比如 readline, ask, choose 和 prompt 都是可用的。

Progress(iterator) 可以使你快速地从迭代器来创建一个进度条。简单地打包一个 slow 迭代器并迭代就会生成一个不错的基于用户屏幕宽度的文本进度条,同时会显示剩余时间。如果你想给 fast 迭代器创建一个进度条,并且在循环中包含代码,那么请使用 Progress.wrap或者 Progress.range。例如:

foriinProgress.range(10):

time.sleep(1)

如果在终端中有其他输出,但是仍然需要一个进度条,请传递 has_output=True 参数来禁止进度条清除掉历史输出。

在 plumbum.cli.image 中提供了一个命令行绘图器(Image)。它可以绘制一个类似 PIL 的图像:

Image().show_pil(im)

Image 构造函数接受一个可选的 size 参数(如果是 None,那么默认是当前终端大小)和一个字符比例,该比例来自当前字符的高度和宽度的度量,默认值是 2.45。如果设置为 None,ratio 将会被忽略,图像不再被限制成比例缩放。要直接绘制一个图像,show 需要一个文件名和一对参数。show_pil 和 show_pil_double 方法直接接受一个 PIL-like 对象。为了从命令行绘制图像,该模块可以直接被运行:python -m plumbum.cli.image myimage.png。

要获取帮助列表和更多的信息请参见 api docs[7]。

请参阅◈ filecopy.py[8] 示例◈ geet.py[9] - 一个可运行的使用子命令的示例◈ RPyC[10] 已经将基于 bash 的编译脚本换成了 Plumbum CLI。这是多么简短和具有可读性[11]◈ 一篇博客[12],讲述 CLI 模块的理论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值