python3 编写命令行工具

命令行程序对比GUI程序有很多优点:编写简单,参数形式统一,便于自动化,使用python编写命令行实现单一功能,然后用shell调用比较方便。
此外命令行解析,用来调试程序也比较方便,不用到源码里改程序的参数了,用命令行指定就比较灵活。
很多功能简单的程序,不涉及复杂的交互,用命令行比较方便,更好用。

01.注释运行环境

如果在linux系统上运行,通过注释运行环境,然后我们使用chmod a+x赋予执行权限,就可以直接运行文件不用指明解释器了,比较方便。

#!/usr/bin/env python

如果在windows系统中这个注释是没有效果的。windows系统通常是根据文件后缀选择打开的工具。

02.命令行解析工具argparse的使用

argparse是python标准库自带的命令行解析工具,功能比较强大。因为标准库自带,所以一般我们用这个写命令行就行了,比较方便。

我们先来编写一个最简单的程序:

%%writefile 'arghelp.py'
import argparse
parser = argparse.ArgumentParser()
parser.parse_args()
%%cmd
python arghelp.py -h
Microsoft Windows [版本 10.0.17763.557]
(c) 2018 Microsoft Corporation。保留所有权利。

D:\code\Projects\jupyterNotes\python3\python3笔记>python arghelp.py -h
usage: arghelp.py [-h]

optional arguments:
  -h, --help  show this help message and exit

D:\code\Projects\jupyterNotes\python3\python3笔记>

我们在cmd中执行这个程序,发现可以打印出唯一一条帮助文档。
--help -h是默认会添加的选项,不需要定制

001. 位置参数

下面我们写一个简单的计算乘幂的程序来演示,位置参数的添加。

%%writefile 'calpower.py'
import argparse
parser = argparse.ArgumentParser()
# 加进去的字符串会被当成选项,注意类型直接写,不用字符串,这样子不带连接符的就是位置参数,是必须的。
# 如果带连接符就是可选参数
parser.add_argument('base',help='指数运算的底',type=int)
parser.add_argument('power',help='指数运算的幂',type=int)
args = parser.parse_args()
print ('{}th power of {} is {}'.format(args.power,args.base,args.base**args.power))
%%cmd
python calpower.py 2 3
Microsoft Windows [版本 10.0.17763.557]
(c) 2018 Microsoft Corporation。保留所有权利。

D:\code\Projects\jupyterNotes\python3\python3笔记>python calpower.py 2 3
3th power of 2 is 8

D:\code\Projects\jupyterNotes\python3\python3笔记>

002.可选参数

带连接符的参数就会被当做可选参数
下面我们添加一个常见的功能,就是输出通常有简单模式,复杂模式两种

%%writefile 'test/calpower.py'
import argparse
parser = argparse.ArgumentParser()
# 加进去的字符串会被当成选项,注意类型直接写,不用字符串,这样子不带连接符的就是位置参数,是必须的。
# 如果带连接符就是可选参数
parser.add_argument('base',help='指数运算的底',type=int)
parser.add_argument('power',help='指数运算的幂',type=int)
# 可以直接添加简短复杂两种,总之加进去的字符串会被当成选项
# 这里是个开关选项只有true和flase两个值,所以我们默认设为true
parser.add_argument('-v','--verbose', help="increase output verbosity", action="store_true")
args = parser.parse_args()
if args.verbose:
    print('verbose on')
    print ('{}th power of {} is {}'.format(args.power,args.base,args.base**args.power))
else:
    print(args.base**args.power)
%%cmd
python test/calpower.py  2 3 
Microsoft Windows [版本 10.0.17763.557]
(c) 2018 Microsoft Corporation。保留所有权利。

D:\code\Projects\jupyterNotes\python3\python3笔记>python test/calpower.py  2 3 
8

D:\code\Projects\jupyterNotes\python3\python3笔记>
%%cmd
python test/calpower.py -v 2 3 
Microsoft Windows [版本 10.0.17763.557]
(c) 2018 Microsoft Corporation。保留所有权利。

D:\code\Projects\jupyterNotes\python3\python3笔记>python test/calpower.py -v 2 3 
verbose on
3th power of 2 is 8

D:\code\Projects\jupyterNotes\python3\python3笔记>

也就是说action='store_true'意思是,当有这个选项时就把值设置为true

add_argument还有一些参数,比如说 choices=[0, 1, 2],这样就可以在指定范围里选
count参数,这样就可以用来指定输出的级别
default可以指定默认值

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display the square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity",default=0)
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity == 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

003.互斥参数

使用add_mutually_exclusive_group()这个方法可以添加互斥参数

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
else:
    print("{}^{} == {}".format(args.x, args.y, answer))

004.添加工具的描述

只需要在创建parser对象时给出description参数即可

import argparse

parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
else:
    print("{}^{} == {}".format(args.x, args.y, answer))

005.详细参数说明

parser = argparse.ArgumentParser(prog=None, usage=None, description=None, epilog=None, parents=[], formatter_class=argparse.HelpFormatter, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='error', add_help=True)

创建命令行解析对象

其中的参数:

  • prog - 程序的名字(默认:sys.argv[0])

  • usage - 描述程序用法的字符串(默认:从解析器的参数生成)

  • description - 参数帮助信息之前的文本(默认:空)

  • epilog - 参数帮助信息之后的文本(默认:空)

  • parents - ArgumentParser 对象的一个列表,这些对象的参数应该包括进去,像有时候需要解析非常复杂的关键字参数,比如像git那样的,

  • formatter_class - 定制化帮助信息的类

  • prefix_chars - 可选参数的前缀字符集(默认:‘-‘)

  • fromfile_prefix_chars - 额外的参数应该读取的文件的前缀字符集(默认:None)

  • argument_default - 参数的全局默认值(默认:None)

  • conflict_handler - 解决冲突的可选参数的策略(通常没有必要)

  • add_help - 给解析器添加-h/–help 选项(默认:True)

parser.add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest])

增加命令行参数,方法的参数说明如下:

  • name or flags 命令行参数名或者选项,如上面的address或者-p,--port.其中命令行参数如果没给定,且没有设置defualt,则出错。但是如果是选项的话,则设置为None,add_argument() 方法必须知道期望的是可选参数,比如-f 或者--foo,还是位置参数,比如一个文件列表。传递给add_argument() 的第一个参数因此必须是一个标记序列或者一个简单的参数名字。例如,一个可选的参数可以像这样创建:

  • action action 关键字参数指出应该如何处理命令行参数。支持的动作有:

    • 'store' - 只是保存参数的值。这是默认的动作

    • 'store_const' - 保存由const关键字参数指出的值。(注意const关键字参数默认是几乎没有帮助的None。)'store_const'动作最常用于指定某种标记的可选参数

    • 'store_true'和'store_false' - 它们是'store_const' 的特殊情形,分别用于保存值True和False。另外,它们分别会创建默认值False 和True。

    • 'append' - 保存一个列表,并将每个参数值附加在列表的后面。这对于允许指定多次的选项很有帮助。示例用法:

    • 'append_const' - 保存一个列表,并将const关键字参数指出的值附加在列表的后面。(注意const关键字参数默认是None。)'append_const' 动作在多个参数需要保存常量到相同的列表时特别有用。例如:

    • 'count' - 计算关键字参数出现的次数。例如,这可用于增加详细的级别:

    • 'help' - 打印当前解析器中所有选项的完整的帮助信息然后退出。默认情况下,help动作会自动添加到解析器中。参见ArgumentParser以得到如何生成输出信息。

    • 'version' - 它期待version=参数出现在add_argument()调用中,在调用时打印出版本信息并退出:

  • nargs 命令行参数的个数,一般使用通配符表示,其中,'?'表示只用一个,'*'表示0到多个,'+'表示至少一个

  • default 默认值
    type 参数的类型,默认是字符串string类型,还有float、int,file等类型

  • choices 可以看做是default的扩展,参数的值必须在choices的范围内

  • required 一般情况下,argparse模块假定-f和--bar标记表示可选参数,它们在命令行中可以省略。如果要使得选项是必需的,可以指定True作为required=关键字参数的值给add_argument()

  • help 和ArgumentParser方法中的参数作用相似,出现的场合也一致

006.子解析

像git这样复杂的命令行工具,会有add push pull等分支,需要用到一个新的方法:
add_subparsers([title][, description][, prog][, parser_class][, action][, option_string][, dest][, help][, metavar])

  • title - 在输出的帮助中子解析器组的标题;默认情况下,如果提供description参数则为“subcommands”,否则使用位置参数的标题
  • description - 在输出的帮助中子解析器组的描述,默认为None
  • prog - 与子命令的帮助一起显示的使用帮助信息,默认为程序的名字和子解析器参数之前的所有位置参数
  • parser_class - 用于创建子解析器实例的类,默认为当前的解析器(例如ArgumentParser)
  • dest - 子命令的名字应该存储的属性名称;默认为None且不存储任何值
  • help - 在输出的帮助中子解析器中的帮助信息,默认为None
  • metavar - 在帮助中表示可用的子命令的字符串;默认为None并以{cmd1, cmd2, ..}的形式表示子命令
parser = argparse.ArgumentParser()
parser.add_argument('--foo', action='store_true', help='foo help')

subparsers = parser.add_subparsers(help='sub-command help')
parser_a = subparsers.add_parser('a', help='a help')
parser_a.add_argument('bar', type=int, help='bar help')

parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')
parser.parse_args(['a', '12'])
Namespace(bar=12, foo=False)
parser.parse_args(['--foo', 'b', '--baz', 'Z'])
Namespace(baz='Z', foo=True)

处理子命令的一个特别有效的方法是将add_subparsers()方法和set_defaults() 调用绑在一起使用,这样每个子命令就可以知道它应该执行哪个Python 函数。例如:


def foo(args):
    print(args.x * args.y)
def bar(args):
    print('((%s))' % args.z)
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser_foo = subparsers.add_parser('foo')
parser_foo.add_argument('-x', type=int, default=1)
parser_foo.add_argument('y', type=float)
parser_foo.set_defaults(func=foo)

parser_bar = subparsers.add_parser('bar')
parser_bar.add_argument('z')
parser_bar.set_defaults(func=bar)

007.参数分组

很多时候参数是相互配合使用的,这就可以用add_argument_group(title=None, description=None)分组

parser = argparse.ArgumentParser(prog='PROG', add_help=False)
group1 = parser.add_argument_group('group1', 'group1 description')
group1.add_argument('foo', help='foo help')
group2 = parser.add_argument_group('group2', 'group2 description')
group2.add_argument('--bar', help='bar help')

008.多级子命令

如果我们的命令行工具有更加复杂的子命令解析需求,那么我们可以使用如下的方式做扩展:

class Command:

    def __init__(self, argv):
        parser = argparse.ArgumentParser(
            description='xxxxxx',
            usage='''xxxx <command> [<args>]

The most commonly used xxx commands are:

   clean       clean a project
   install     install a package


''')
        parser.add_argument('command', help='Subcommand to run')
        # parse_args defaults to [1:] for args, but you need to
        # exclude the rest of the args too, or validation will fail
        self.argv = argv
        args = parser.parse_args(argv[0:1])
        if not hasattr(self, args.command):
            print('Unrecognized command')
            parser.print_help()
            exit(1)
        # use dispatch pattern to invoke method with same name
        getattr(self, args.command)()


    def clean(self):
        parser = argparse.ArgumentParser(
            description='clean a project')
        parser.add_argument(
            '-A', '--all', action='store_true')
        parser.set_defaults(func=clean)
        args = parser.parse_args(self.argv[1:])
        args.func(args)

    def install(self):
        parser = argparse.ArgumentParser(
            description='install a package for this project')
        parser.add_argument('packages', nargs='?', type=str, default="DEFAULT")
        parser.add_argument(
            '-D', '--dev', action='store_true')
        parser.add_argument(
            '-T', '--test', action='store_true')
        parser.add_argument(
            '-A', '--all', action='store_true')
        parser.set_defaults(func=install)
        args = parser.parse_args(self.argv[1:])
        args.func(args)
        print("install done!")

def main(argv: Sequence[str]=sys.argv[1:]):
    Command(argv)

03.其他命令行解析工具

click是一个很符合Python编程风格的命令行解析,支持构建比较复杂的命令行工具。

fire是谷歌开源的命令行解析工具,可以直接根据函数参数解析成命令行,自动生成

docopt所见即所得的命令行解析工具,利用文件的__doc__,写完文档注释你的命令行解析也就实现了。

用<>包裹表示参数,如果参数后面有...则表示参数是列表
用[]包裹选项
用()包裹必选内容
用|区分选项

下面是一个实例:

%%writefile test/sqrt_doc.py
#!/usr/bin/env python
# coding:utf-8 
u"""
Usage: 
  test1.py [option] <num>...
  test1.py (-v|--version)
  test1.py (-a|--all)
  test1.py (-h|--help)


Options:
  -h --help      帮助
  -v --version   显示版本号.
  -a --all       显示全部参数
"""

from docopt import docopt
from math import sqrt
__version__="0.1.0"



def version():
    return "version:"+__version__

def main():
    args = docopt(__doc__)

    if args.get("-h") or args.get("-help"):
        print(__doc__)
    elif args.get("-v") or args.get("--version"):
        print(__version__)
    elif args.get("-a") or args.get("--all"):
        print(args)
    elif args.get("<num>"):
        print(" ".join(map(lambda x :str(sqrt(float(x))),args.get("<num>"))))
    else:
        print("wrong args!")
        print(__doc__)



if __name__ == '__main__':
    main()
Writing test/sqrt_doc.py

04.命令行进度条

tqdm是一个进度条工具,除了可以给命令行工具增加进度条看出进度外,还可以用于jupyter-notebook

tqdm模块的tqdm类是这个包的核心,所有功能都是在它上面衍生而来

tqdm类 可以包装可迭代对象,它的实例化参数有:

  • desc : str, optional 放在bar前面的描述字符串

  • total : int, optional 显示多长

  • leave : bool, optional 结束时时保留进度条的所有痕迹。

  • file : io.TextIOWrapper or io.StringIO, optional 输出到文件

  • ncols : int, optional 自定义宽度

  • mininterval : float, optional 更新最短时间

  • maxinterval : float, optional 更新最大时间

  • miniters : int, optional 每次更新最小值

  • ascii : bool, optional 使用ascii碼显示

  • disable : bool, optional 是否禁用整个progressbar

  • unit : str, optional 显示的更新单位

  • unit_scale : bool, optional 根据单位换算进度

  • dynamic_ncols : bool, optional 可以不断梗概ncols的环境

  • smoothing : float, optional 用于速度估计的指数移动平均平滑因子(在GUI模式中忽略)。范围从0(平均速度)到1(当前/瞬时速度)[默认值:0.3]。

  • bar_format : str, optional 指定自定义栏字符串格式。可能会影响性能

  • initial : int, optional 初始计数器值。重新启动进度条时有用[默认值:0]。

  • position : int, optional 指定打印此条的线偏移(从0开始)如果未指定,则为自动。用于一次管理多个条

下面写几个程序实例

# 900万次循环
from tqdm import tqdm
for i in tqdm(range(int(9e6)),desc="test:"):
    pass
test:: 100%|████████████████████████████████████████████████████████████| 9000000/9000000 [00:01<00:00, 4813939.90it/s]
for i in tqdm(range(int(9e6)),desc="test",dynamic_ncols=True):
    pass
test: 100%|█████████████████████████████████████████████████████████████| 9000000/9000000 [00:01<00:00, 4998333.08it/s]
import time
# 使用with语句手动更新
with tqdm(total=100) as bar:
    for i in range(10):
        time.sleep(0.5)
        bar.update(10)
100%|████████████████████████████████████████████████████████████████████████████████| 100/100 [00:05<00:00, 19.94it/s]

05.为命令行工具自动创建gui

Gooey是一个可以将python命令行自动转成gui的工具,它依赖wxpython,下面给出的的例子是吧一个求开平方的命令行工具转化为gui工具

这个工具貌似挺方便,以后简单的gui都不用写了,直接自动生成。

%%writefile src/python/std/sqrt_std_gui.py
#!/usr/bin/env python3
import argparse
from math import sqrt
from gooey import Gooey, GooeyParser


__version__="0.1.0"

def sqrtarg(number):
    return sqrt(number)

def version():
    return "version:"+__version__
@Gooey(language='chinese')
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("number", type=int, help=u"求开根的参数")
    parser.add_argument("-v","--version", help=u"查看版本号",action="store_true")

    args = parser.parse_args()

    if args.version:
        print(version())
    if args.number:
        print(sqrtarg(args.number))

if __name__ == '__main__':
    main()
    

06.命令行工具发布

下面介绍一个用setup.py把脚本安装到python的脚本位置的例子

from distutils.core import setup
import os
pathroot = os.path.split(os.path.realpath(__file__))[0]
setup(
    name='sqrt_doc',
    version='0.1.0',

    scripts=[pathroot+'/sqrt_doc.py']
)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容