当你要实现一个命令行程序的时候,或许第一个想到的就是用Python来实现。例如,CentOS上著名的包管理工具yum就是基于Python实现的。在Python的世界里,有很多命令行库,每一个都有自己的特点。但我们往往不知道其背后的设计理念,因此在选择时感到迷茫。为什么这些库的作者要重新发明轮子,他是从什么角度思考的,让命令行库“进化”成一种新的、更好用的形式。为了能够更直观地感受命令行库的设计理念,在此之前,我们不妨设计一个名为calc的命令行程序,它可以:支持echo子命令,如果没有提供则处理输入字符串输出任意选项,输出原内容如果提供--lower选项,则输出小写字符串如果提供--upper选项,则输出大写字符串支持eval子命令,调用Python的eval函数进行输入,输出结果(作为例子,我们不考虑安全问题)argparseargparse作为Python的标准库,它可能是你第一个想到的命令行库。argparse的设计理念是为开发者提供最细粒度的控制。换句话说,您需要告诉它基本的细节,例如参数的类型是什么以及对参数执行的操作是什么。在argparse的世界里,你需要:设置解析器作为后续定义参数和解析命令行的基础。如果要执行子命令,则还设置子解析器。定义参数,包括名称、类型、动作、帮助等。action指的是对这个参数的初步处理,是直接保存,还是作为boolean值,还是追加到list等。parseparameters根据参数编写业务逻辑下面的例子是基于argparse的calc程序:`importargparsedefecho_text(args):ifargs.lower:print(args.text.lower())elifargs.upper:print(args.text.upper())else:print(args.text)defeval_expression(args):print(eval(args.expression))#1.设置解析器parser=argparse.ArgumentParser(description='CalculatorProgram.')subparsers=parser.add_subparsers()#2.定义参数#2.1echo子命令#echosubparserecho_parser=subparsers.add_parser('echo',help='Echoinputtextinmultipleforms')#添加位置参数textecho_parser.add_argument('text',help='Inputtext')#--lower/--upper互斥,需要设置互斥组echo_group=echo_parser.add_mutually_exclusive_group()#添加选项参数--lower/--upper,其中action为c把它变成一个布尔变量echo_parser.add_argument('--lower',action='store_true',help='Lowerinputtext')echo_parser.add_argument('--upper',action='store_true',help='Upperinputtext')#设置这条命令的处理函数echo_parser.set_defaults(handle=echo_text)#evalsubparsereval_parser=subparsers.add_parser('eval',help='evalinputexpressionandreturnresult')#addpositionparameterexpressioneval_parser.add_argument('expression',help='Expressiontoeval')#设置该命令的处理函数eval_parser.set_defaults(handle=eval_expression)#3.解析参数args=parser.parse_args(['echo','--upper','Hello,World'])print(args)#Result:Namespace(lower=True,text='你好,世界',upper=False)#args=parser.parse_args(['eval','1+2*3'])#print(args)#Result:Namespace(expression='1+2*3')#4.业务逻辑处理args.handle(args)`从上面的例子可以看出,要实现子命令,需要添加相应的子解析器,最重要的是定义参数,需要通过add_argument明确告诉argparse参数length是什么,如何处理:是位置参数text/expression,还是选项参数--lower/--upper如果是选项参数,是否是相互exclusive参数以什么形式存储,比如action='store_true'表示以Boolean形式存储。它的优点是灵活,所有的功能都涵盖了;但缺点是定义和处理分离,尤其是当程序功能复杂时,会变得更加凌乱和不直观,难以理解和维护。docopt有人喜欢命令式的写argparse,有人喜欢声明式的写法。而docopt恰好就是这样一个命令行库。设计它的初衷是,对于熟悉命令行程序帮助信息的开发者来说,通过编写帮助信息来直接描述整个命令行参数定义的元信息会更加容易和快捷。这种声明性语法描述比参数的过程定义更简单、更直观。在docopt的世界里,需要:定义接口描述/帮助信息,这一步是它的特性和关键解析参数,获取字典,根据参数编写业务逻辑下面的例子是一个基于docopt的calc程序:`_#1.定义接口描述/帮助信息_"""计算器程序。用法:calcecho[--lower|--upper]calceval命令:echo以多种形式回显输入文本evalEvalinputexpressionandreturnresultOptions:-h--helpShowhelp--lowerLowerinputtext--upperUpperinputtext"""fromdocoptimportdocoptdefecho_text(args):ifargs['--lower']:print(args复制代码[''].lower())elifargs['--upper']:print(args[''].upper())else:print(args[''])defeval_expression(args):print(eval(args['']))#2.解析命令行args=docopt(__doc__,argv=['echo','--upper','Hello,World'])#结果:{'--lower':False,'--upper':True,'':None,'':'Hello,World','echo':True,'eval':False}print(args)#3.业务逻辑ifargs['echo']:echo_text(args)elifargs['eval']:eval_expression(args)`从上面的例子可以看出,我们通过文档字符串__doc__来定义接口描述,相当于argparse中一系列参数定义的行为,然后docopt会使用这个元信息来转换命令行参数转换为字典业务逻辑需要处理字典。与argparse:相比,对于更复杂的命令,docopt在命令和参数元信息的定义上会更简单。在业务逻辑处理方面,argparse在一些简单参数的处理上会更加方便,命令和处理函数可以方便的路由(比如例子中的情况);相对来说,将docopt转为字典后,将所有的处理都转移到业务逻辑上会更复杂一些。不管是argparse还是docopt,元信息的定义和处理都是分开的。命令行程序本质上是定义参数并对其进行处理,处理参数的逻辑必须与定义的参数相关联。能否通过函数和装饰器来实现处理参数逻辑和定义参数的关联?click被设计为完全以这种方式使用。装饰器是一种优雅的语法糖,它们是元信息定义和处理逻辑之间的粘合剂,从而暗示这两种方式是相关的。相比前两个命令行库的路由实现,优雅多了。在点击的世界里:通过装饰器定义命令和参数的元信息用这个装饰器来装饰处理函数对,就这么简单。以下示例是基于点击的计算程序:`importsysimportclicksys.argv=['calc','echo','--upper','Hello,World']@click.group(help='CalculatorProgram.')defcli():pass#2.定义参数@cli.command(name='echo',help='Echoinputtextinmultipleforms')@click.argument('text')@click.option('--lower',is_flag=True,help='Lowerinputtext')@click.option('--upper',is_flag=True,help='Upperinputtext')#1.业务逻辑defecho_text(text,lower,upper):iflower:print(text.lower())elifupper:print(text.upper())else:print(text)@cli.command(name='eval',help='Eval输入表达式andreturnresult')@click.argument('expression')defeval_expression(expression):print(eval(expression))cli()`从上面的例子我们可以看出元信息定义和处理逻辑是无缝绑定在一起的,并且可以直观的看出相应的参数会被如何处理。当需要处理的参数数量较多时,这种优势尤为突出。在处理函数中,接收不再是像argparse或docopt那样包含所有参数的变量,而是一个特定的参数变量,这使得处理逻辑在参数方面更容易使用。此外,click还内置了很多实用工具和增强能力,比如参数自动补全、分页支持、颜色、进度条等功能,可以有效提高开发效率。前三火库虽然足够强大,但还是有人认为不够简单。是否还有进一步简化的空间?如果你只是定义一个函数,框架可以推断出参数元信息吗?理论上是可以的。fire使用通用的面向对象的方式来玩命令行。这样的对象可以是类、函数、字典、列表等,更加灵活简单。你不需要定义参数类型,fire会根据输入和参数默认值自动判断,这无疑进一步简化了实现过程。在火的世界里,定义Python对象就足够了。下面的例子是基于fire的calc程序:`importsysimportfiresys.argv=['calc','echo','"Hello,World"','--upper']#业务逻辑#里面有几个方法class,表示命令行程序有几个同名的命令视为选项参数--lower/--upper,__#指定True,不指定False_defecho(self,text,lower=False,upper=False):"""以多种形式回显输入文本"""iflower:print(text.lower())elifupper:print(text.upper())else:print(text)defeval(self,expression):"""Eval输入表达式并返回结果"""print(eval(expression))fire.Fire(Calc)`从上面的例子我们可以看出使用fire已经足够简单了,一切都是按照约定来推断的,包括支持哪些命令,每个命令有哪些参数和选项接受。这个方法可以说是足够Pythonic了。与click相比,fire整合了命令行参数的定义和函数参数的定义。有了它,我们真的只需要关注业务逻辑。但简单性通常也意味着它需要满足复杂的需求。仅通过默认值推导命令行参数所能表达的情况是有限的。例如,互斥选项和位置参数的类型限制无法通过框架表达,只能通过业务逻辑判断。typer那么如何在保持像火一样简单的实现的同时,增强参数元信息的表达能力呢?由于默认参数的功能有限,那么使用Python3类型注释呢?打字机站在点击巨人的肩膀上。借助Python3的类型注解,既满足了简单直观的书写需求,又达到了应对复杂场景的目的。它可以被描述为一个现代命令行库。在typer的世界里,业务逻辑也是直接写的。与fire略有不同的一点是,使用类型注解和默认值来表达参数元信息定义。以下示例是一个基于类型的计算程序:`importsysimporttypersys.argv=['calc','echo','"Hello,World"','--upper']cli=typer.Typer(help='CalculatorProgram.')#定义命令回显,以及处理函数#text无默认值,视为位置参数,类型为字符串#lower/upper类型为bool,默认值为False,以及它被视为选项--lower/--upper,#并指定为True,未指定为False@cli.command(name='echo')defecho_text(text:str,lower:bool=False,upper:bool=False):"""以多种形式回显输入文本"""iflower:print(text.lower())elifupper:print(text.upper())else:print(text)#definecommandeval,和处理函数#expression没有默认值,被当做位置参数,类型为字符串@cli.command(name='eval')defeval_expression(expression:str):"""Eval输入表达式并返回结果"""print(eval(expression))cli()`从上面的例子中可以看出n与click相比,省去了繁琐的参数元信息定义,取而代之的是类型注解;与火相比,它定义元信息的能力大大增强。您可以将默认值指定为typer.Option或typer.Argument以进一步扩展参数和选项的语义。可以说typer在简单性和灵活性上达到了完美的平衡。横向对比最后我们对argparse、docopt、click、fire、typer库的功能和特性做一个横向对比:argpasedocoptclickfiretyper使用步数4步3步2步1步1步使用步数settheparsertodefineparametersandparsethecommandlineBusinessLogicDefinitionInterfaceDescriptionParsingCommandLineBusinessLogic业务逻辑定义参数业务逻辑1.业务逻辑选项参数(eg--sum)?????PositionalParameters(egXY)?????参数默认值?????互斥选项(如--car和--bus只能二选一)???通过第三方库支持??可变参数(如指定多个--files)?????嵌套/父子命令?????工具箱?????链式命令调用?????TypeConstraints?????Python的命令行库是多种多样的和独特,他们不是重新发明轮子产品,思想背后值得借鉴。结合横向对比总结,可以选择符合使用场景的库。如果多个库兼容,则选择您喜欢的样式。原文链接本文为阿里云原创内容,未经许可不得转载。