http.server
可以使用 -h
查看帮助。这种自定义的命令行工具对用户使用程序非常有帮助,我们一起学习是如何实现命令工具的。
先看看展示:
python -m http.server -h usage: server.py [-h] [--cgi] [--bind ADDRESS] [--directory DIRECTORY] [port] positional arguments: port Specify alternate port [default: 8000] optional arguments: -h, --help show this help message and exit --cgi Run as CGI Server --bind ADDRESS, -b ADDRESS Specify alternate bind address [default: all interfaces] --directory DIRECTORY, -d DIRECTORY Specify alternative directory [default:current directory]
从 http.server
模块帮助信息中可以看到:
- usage 提供了使用的完整的使用示例
- 位置参数 port , 可以自定义服务端口, 默认值 8000
- 可选参数,叫关键字参数可能更合适些:
- -h/--help 展示帮助信息
- --cgi 使用cgi服务,看起来是个bool值
- --bind/-b 服务ip地址,默认是所有地址
- --directory/-d 服务文件目录,默认当前目录
我们把上面的描述换个方式,使用python函数定义,这样就很容易理解位置参数和关键字参数了:
def http_server(port, cgi=False, bind="all interfaces", directory="current directory"): pass
函数和命令行参数定义有所不同:
- 关键字参数有长短选项的写法,
-b
和--bind
都可以 - port会限定数值是int类似型
- usage信息和help信息是如何自动生成的
带着这几个疑问,我们去python源码中查找答案。本文分下面几个部分:
- sys.argv 简介
- getopt 解析
- optparse 解析
- argparser 解析
- 小结
- 小技巧
sys.argv 简介
编写测试脚本
# simple.py import sys if __name__ == "__main__": print(type(sys.argv), sys.argv)
使用下面的命令运行测试脚本
python simple.py 1 a bbb c=2 d@3 <class 'list'> ['simple.py', '1', 'a', 'bbb', 'c=2', 'd@3']
可以看到 sys.argv
是一个列表,其中包含了被传递给 Python 脚本的命令行参数, argv[0] 为脚本的名称。这是命令工具的起点,所有命令行工具从这里派生扩展。
getopt 解析
我在上个例子中使用了 d@3
, 这是搞笑的。命令行参数有约定的惯例和规范,在unix-shell中由getopt函数实现,具体可以看参考链接中的wiki部分。python中也提供了 getopt
实现。先看看如何使用,短选项使用单个 -
前缀:
import getopt args = '-a -b -cfoo -d bar a1 a2'.split() print(args) # ['-a', '-b', '-cfoo', '-d', 'bar', 'a1', 'a2'] optlist, args = getopt.getopt(args, 'abc:d:') print(optlist) # [('-a', ''), ('-b', ''), ('-c', 'foo'), ('-d', 'bar')] print(args) # ['a1', 'a2']
长选项使用两个--
前缀:
s = '--condition=foo --testing --output-file abc.def -x a1 a2' args = s.split() print(args) # ['--condition=foo', '--testing', '--output-file', 'abc.def', '-x', 'a1', 'a2'] optlist, args = getopt.getopt(args, 'x', [ 'condition=', 'output-file=', 'testing']) print(optlist) # [('--condition', 'foo'), ('--testing', ''), ('--output-file', 'abc.def'), ('-x', '')] print(args) # ['a1', 'a2']
getopt
返关键字参数optlist和位置参数args。 关键字有长关键字 --connddition
和短关键字 -c
两个名称,为什么会有长短两种方式,我的理解是长关键字语义更明确,单独的字母 c 很难知道代表的含义,而使用单词 condditon 则一目了然; 短关键字使用更便捷,只需要敲一个字母,还可以多个参数合并,比如 ls -lah
。
在 quopri
中演示了如何使用 getopt
实现命令行参数解析:
# quopri import getopt try: opts, args = getopt.getopt(sys.argv[1:], 'td') except getopt.error as msg: sys.stdout = sys.stderr print(msg) print("usage: quopri [-t | -d] [file] ...") # 帮助信息 print("-t: quote tabs") print("-d: decode; default encode") sys.exit(2) ... for o, a in opts: # 解析关键字参数 if o == '-t': tabs = 1 if o == '-d': deco = 1 for file in args: # 解析位置参数 ...
主要的getopt
函数代码如下:
# getopt def getopt(args, shortopts, longopts = []): opts = [] longopts = list(longopts) while args and args[0].startswith('-') and args[0] != '-': ... if args[0].startswith('--'): opts, args = do_longs(opts, args[0][2:], longopts, args[1:]) else: opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:]) # args[0][1:] 移除前缀 return opts, args
- 3个参数:待解析参数,短参数定义,长参数定义。 短参数定义使用字符串,比如
abc:d:
;长参数使用数组,比如:['condition=', 'output-file=', 'testing']
- 使用while循环持续的解析关键字参数,关键字参数解析完成后剩余的就是位置参数args
- 长参数使用do_longs解析,短参数使用do_shorts解析
短参数的解析方法
def do_shorts(opts, optstring, shortopts, args): while optstring != '': # while循环 opt, optstring = optstring[0], optstring[1:] # 截取参数关键字和剩余字符 if short_has_arg(opt, shortopts): if optstring == '': if not args: raise GetoptError(_('option -%s requires argument') % opt, opt) optstring, args = args[0], args[1:] # 贪婪后面的参数 '-d', 'bar' optarg, optstring = optstring, '' # 取甚于部分 -cfoo else: optarg = '' opts.append(('-' + opt, optarg)) # 无参数 '-a', '-b return opts, args def short_has_arg(opt, shortopts): for i in range(len(shortopts)): if opt == shortopts[i] != ':': return shortopts.startswith(':', i+1) # 判断之后是否跟着:字符 raise GetoptError(_('option -%s not recognized') % opt, opt)
以 args=['-a', '-b', '-cfoo', '-d', 'bar', 'a1', 'a2']
和 shortopts='abc:d:'
为例,介绍一下解析的执行过程:
- -a, -b 无需参数值,,在shortopts仅
ab
,没有:
后缀,未命中short_has_arg,返回两个元祖 ('-a', ''), ('-b', '') - -cfoo 需要参数值,在shortopts中有
c:
,命中short_has_arg,返回 ('-c', 'foo') - -d, 需要参数值,在shortopts中有
d:
,命中short_has_arg,并捕获后面跟着的bar,一起返回 ('-d', 'bar') - a1, a2 位置参数最后剩余
这样我们就很清楚 abc:d:
的含义了,每个字符是一个参数,如果需要参数值,则后面跟一个:
字符。长参数的解析方法,比较类似,就不再赘述了。
optparse 解析
从quopri
源码中可以看到 getopt
提供的方法比较单薄,还需要手工print(usage && help)
信息,解析后的参数使用也不直观, 需要按照位置获取。接下来登场的是 optparse
, 可以在cProfile中看到使用示例:
# cProfile.py from optparse import OptionParser usage = "cProfile.py [-o output_file_path] [-s sort] scriptfile [arg] ..." # 注1 parser = OptionParser(usage=usage) parser.allow_interspersed_args = False parser.add_option('-o', '--outfile', dest="outfile", help="Save stats to <outfile>", default=None) # 注2 parser.add_option('-s', '--sort', dest="sort", help="Sort order when printing to stdout, based on pstats.Stats class", default=-1) (options, args) = parser.parse_args() runctx(code, globs, None, options.outfile, options.sort) # 注3
查看效果:
python -m cProfile -h Usage: cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ... Options: -h, --help show this help message and exit -o OUTFILE, --outfile=OUTFILE Save stats to <outfile> -s SORT, --sort=SORT Sort order when printing to stdout, based on pstats.Stats class -m Profile a library module
optparse对比getopt:
- 可以设置usage信息 (注1)
- 自动收集参数帮助,生成help信息 (注2)
- option使用比较直观,可以使用
options.outfile
获取参数值 (注3)
官方的文档中介绍optparse难以扩展,已经被废弃,推荐使用基于它的argparse替代。但是我们还是不放过它,这对理解argparser有帮助。
optparse 模块结构
optparse的模块类图:
可以看到optparse模块主要就是 OptionParser
, Option
和HelpFormatter
三个类。
optparse 实现
parser的使用模版,就是下面3行代码:创建对象,添加option和进行参数解析并返回
parser = OptionParser(usage=usage) parser.add_option('-o', '--outfile', dest="outfile", help="Save stats to <outfile>", default=None) # 添加多个参数... parser.parse_args() # 自动获取sys.argv 不需要传入
先查看 OptionParser 对象创建
class OptionContainer: def __init__(self, option_class, conflict_handler, description): # Initialize the option list and related data structures. # This method must be provided by subclasses, and it must # initialize at least the following instance attributes: # option_list, _short_opt, _long_opt, defaults. self._create_option_list() # 抽象方法,由子类实现,这时候可能没有abc模块,抽象方法使用注释进行要求 self.option_class = option_class # 选项类,可以由用户扩展 self.conflict_handler = handler self.description = description class OptionParser(OptionContainer): def __init__(self, usage=None, option_list=None, option_class=Option, version=None, conflict_handler="error", description=None, formatter=None, add_help_option=True, prog=None, epilog=None): OptionContainer.__init__( self, option_class, conflict_handler, description) self.usage = usage self.version = version if formatter is None: formatter = IndentedHelpFormatter() # 默认帮助类 self.formatter = formatter self.formatter.set_parser(self) self._populate_option_list(option_list, add_help=add_help_option) self._init_parsing_state()
上面代码创建了OptionParser对象,下面代码初始化了部分属性
def _create_option_list(self): self.option_list = [] self.option_groups = [] self._short_opt = {} # single letter -> Option instance self._long_opt = {} # long option -> Option instance self.defaults = {} # maps option dest -> default value def _add_help_option(self): self.add_option("-h", "--help", action="help", help=_("show this help message and exit")) def _populate_option_list(self, option_list, add_help=True): ... if self.version: self._add_version_option() # version-option默认未开启 if add_help: self._add_help_option() # 默认添加help-option def _init_parsing_state(self): # These are set in parse_args() for the convenience of callbacks. self.rargs = None # 初始化 self.largs = None self.values = None
add_option实现,可以看到很熟悉的长参数和短参数
def add_option(self, *args, **kwargs): if isinstance(args[0], str): option = self.option_class(*args, **kwargs) # Option self.option_list.append(option) option.container = self for opt in option._short_opts: self._short_opt[opt] = option # 长参数 for opt in option._long_opts: self._long_opt[opt] = option # 短参数 if option.dest is not None: # option has a dest, we need a default if option.default is not NO_DEFAULT: self.defaults[option.dest] = option.default elif option.dest not in self.defaults: self.defaults[option.dest] = None return option
Option存储参数设置, 构建长选项和短选项列表,并check选项是否合法:
class Option: def __init__(self, *opts, **attrs): self._short_opts = [] self._long_opts = [] # 为什么option要有short和long两个数组 ... self._set_opt_strings(opts) # Set all other attrs (action, type, etc.) from 'attrs' dict self._set_attrs(attrs) for checker in self.CHECK_METHODS: checker(self)
使用前缀判断参数列表:
def _set_opt_strings(self, opts): for opt in opts: if len(opt) < 2: raise elif len(opt) == 2: if not (opt[0] == "-" and opt[1] != "-"): raise self._short_opts.append(opt) else: if not (opt[0:2] == "--" and opt[2] != "-"): raise self._long_opts.append(opt)
option的检查方法比较多,我们看一下的action和type检查
CHECK_METHODS = [_check_action, _check_type, _check_choice, _check_dest, _check_const, _check_nargs, _check_callback] def _check_action(self): if self.action is None: self.action = "store" # 默认原样存储 elif self.action not in self.ACTIONS: raise def _check_type(self): # 判断参数类型 if self.type is None: if self.action in self.ALWAYS_TYPED_ACTIONS: if self.choices is not None: # The "choices" attribute implies "choice" type. self.type = "choice" # 枚举 else: # No type given? "string" is the most sensible default. self.type = "string" else: # Allow type objects or builtin type conversion functions # (int, str, etc.) as an alternative to their names. if isinstance(self.type, type): # 其它类型 self.type = self.type.__name__ if self.type == "str": self.type = "string" if self.type not in self.TYPES: raise if self.action not in self.TYPED_ACTIONS: raise
store-action的使用等解析参数时候再介绍。完成Parser对象的构建后,就是如何使用parse_args解析参数:
def parse_args(self, args=None, values=None): rargs = sys.argv[1:] # 从sys.argv中获取输入 ... values = self.get_default_values() # 获取默认值 ... stop = self._process_args([], rargs, values) # 解析参数
熟悉的参数解析分支:
def _process_args(self, largs, rargs, values): while rargs: # while循环 arg = rargs[0] elif arg[0:2] == "--": # process a single long option (possibly with value(s)) self._process_long_opt(rargs, values) # 处理长参数 elif arg[:1] == "-" and len(arg) > 1: # process a cluster of short options (possibly with # value(s) for the last one only) self._process_short_opts(rargs, values) # 处理短参数 ...
短参数的解析过程:
def _process_short_opts(self, rargs, values): arg = rargs.pop(0) stop = False i = 1 for ch in arg[1:]: # 逐个字符解析 opt = "-" + ch option = self._short_opt.get(opt) # 获取对应的 Option 规则 i += 1 # we have consumed a character if option.takes_value(): # Any characters left in arg? Pretend they're the # next arg, and stop consuming characters of arg. if i < len(arg): rargs.insert(0, arg[i:]) stop = True nargs = option.nargs if len(rargs) < nargs: ... elif nargs == 1: value = rargs.pop(0) # 解析出参数值 else: value = tuple(rargs[0:nargs]) del rargs[0:nargs] else: # option doesn't take a value value = None option.process(opt, value, values, self) # 存储到option
Option存储参数Action的实现:
def process(self, opt, value, values, parser): # And then take whatever action is expected of us. # This is a separate method to make life easier for # subclasses to add new actions. return self.take_action( self.action, self.dest, opt, value, values, parser) def take_action(self, action, dest, opt, value, values, parser): if action == "store": setattr(values, dest, value) # 直接存储 elif action == "store_const": setattr(values, dest, self.const) # 使用定义的常量 elif action == "store_true": setattr(values, dest, True) # int=true elif action == "store_false": setattr(values, dest, False) # int=false elif action == "append": values.ensure_value(dest, []).append(value) # 接受数组 elif action == "append_const": values.ensure_value(dest, []).append(self.const) # 常量数组 elif action == "count": setattr(values, dest, values.ensure_value(dest, 0) + 1) # 计数参数,可以重复使用 elif action == "callback": # 支持回掉 args = self.callback_args or () kwargs = self.callback_kwargs or {} self.callback(self, opt, value, parser, *args, **kwargs) elif action == "help": # 帮助 parser.print_help() parser.exit() elif action == "version": # 版本 parser.print_version() parser.exit() else: raise ValueError("unknown action %r" % self.action) return 1
action的使用,可以看参考链接中的howto部分,介绍的非常详细。接下来重点看一下帮助部分的实现。
# parse def format_help(self, formatter=None): if formatter is None: formatter = self.formatter result = [] if self.usage: result.append(self.get_usage() + "\n") # 输出usage if self.description: result.append(self.format_description(formatter) + "\n") # 输出description result.append(self.format_option_help(formatter)) # 开始option-help ... return "".join(result) def format_option_help(self, formatter=None): formatter.store_option_strings(self) result = [] result.append(formatter.format_heading(_("Options"))) formatter.indent() if self.option_list: result.append(OptionContainer.format_option_help(self, formatter)) # 收集option的帮助 result.append("\n") ... formatter.dedent() return "".join(result[:-1]) def format_option(self, option): result = [] opts = self.option_strings[option] opt_width = self.help_position - self.current_indent - 2 if len(opts) > opt_width: # 输出option的关键字 opts = "%*s%s\n" % (self.current_indent, "", opts) indent_first = self.help_position else: # start help on same line as opts opts = "%*s%-*s " % (self.current_indent, "", opt_width, opts) indent_first = 0 result.append(opts) if option.help: # 输出option帮助信息 help_text = self.expand_default(option) help_lines = textwrap.wrap(help_text, self.help_width) result.append("%*s%s\n" % (indent_first, "", help_lines[0])) result.extend(["%*s%s\n" % (self.help_position, "", line) for line in help_lines[1:]]) elif opts[-1] != "\n": result.append("\n") return "".join(result)
看完处理流程,我们大概还有2个疑问:
- option为什么有 _short_opts 和 _long_opts 2个数组
- 如何使用
options.outfile
获取参数值的
第一个问题,请看代码:
# -o 和 --outfile 都可以表示同一个option parser.add_option('-o', '--outfile', dest="outfile", help="Save stats to <outfile>", default=None) def _set_opt_strings(self, opts): # opts = ['-o', '--outfile'] for opt in opts: if ..: self._short_opts.append(opt) if ..: self._long_opts.append(opt)
第二个问题,请看代码:
# 参数值存储到一个字典 setattr(values, dest, value) ... def parse_args(self, args=None, values=None): ... # 返回参数字典 return (values, args) 复制代码
optparse比较难以扩展,我认为主要是因为这段代码:
def take_action(self, action, dest, opt, value, values, parser): if action == "store": setattr(values, dest, value) elif action == "store_const": setattr(values, dest, self.const) ....
这种if-else的代码逻辑,分支一旦变多,就难以维护。可以考虑用设计模式替换。
argparse
argparse 模块结构
argparser的类图,可以看到继承自optparse,左侧基本一致。只是右侧将option换成了action的实现。
argparse 使用示例
http.server中argparse使用示例
# http.server import argparse parser = argparse.ArgumentParser() parser.add_argument('--cgi', action='store_true', help='Run as CGI Server') parser.add_argument('--bind', '-b', metavar='ADDRESS', help='Specify alternate bind address ' '[default: all interfaces]') parser.add_argument('--directory', '-d', default=os.getcwd(), help='Specify alternative directory ' '[default:current directory]') parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') args = parser.parse_args() if args.cgi: handler_class = CGIHTTPRequestHandler else: handler_class = SimpleHTTPRequestHandler test(HandlerClass=handler_class, port=args.port, bind=args.bind)
和optparse一样的使用模版:创建对象,添加参数,解析参数
parser = argparse.ArgumentParser() parser.add_argument('--cgi', action='store_true', help='Run as CGI Server') args = parser.parse_args() # args.port, args.bind
argparse action实现
由于文章篇幅,我们重点看看action部分的实现,使用了注册模式解决if-else问题:
class _ActionsContainer(object): def __init__(self, description, prefix_chars, argument_default, conflict_handler): super(_ActionsContainer, self).__init__() # set up registries self._registries = {} # 注册中心 # register actions self.register('action', None, _StoreAction) # 注册action类 self.register('action', 'store', _StoreAction) def _pop_action_class(self, kwargs, default=None): action = kwargs.pop('action', default) return self._registry_get('action', action, action) # 获取对应action类 def add_argument(self, *args, **kwargs): # create the action object, and add it to the parser action_class = self._pop_action_class(kwargs) action = action_class(**kwargs) # 创建action对象 def parse_args(self, args=None, namespace=None): for action in self._actions: if action.dest is not SUPPRESS: if not hasattr(namespace, action.dest): if action.default is not SUPPRESS: setattr(namespace, action.dest, action.default) # 执行action对象 # 使用方法 parser.add_argument('--cgi', action='store_true', help='Run as CGI Server') args = parser.parse_args()
小结
最后我们再来简单小结一下:
- 使用sys.argv接受命令行参数
- 命令行参数有位置参数和可选参数
- 可选参数有
-
+单个字符和--
+单词的长参数两种 - 命令行参数的解析模版都是3步:创建解析器,添加解析器规则和解析参数
小技巧
分析参数解析过程中,发现切片的一个特点,索引不会越界:
>>> b =[1,2] >>> b[1:] [2] >>> >>> b[3:] # 安全 [] >>> b[3] # 异常 Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range
Python 能够优雅地处理那些没有意义的切片索引:一个过大的索引值(即下标值大于字符串实际长度)将被字符串实际长度所代替,当上边界比下边界大时(即切片左值大于右值)就返回空字符串。(摘自python-tutorial3)
另外使用gettext支持国际化,也可以插一个眼:
try: from gettext import gettext, ngettext except ImportError: def gettext(message): return message _ = gettext class BadOptionError (OptParseError): ... def __str__(self): return _("no such option: %s") % self.opt_str
参考链接
- docs.python.org/zh-cn/3/lib…
- en.wikipedia.org/wiki/Getopt…
- docs.python.org/zh-cn/3/how…
- www.pythondoc.com/pythontutor…
恰逢春节,博主做一个小彩蛋,送给大家:
def happyNiuYear(): print("牛年大吉大利! "*3) happyNiuYear()