第3章 打造命令行工具

3.1 与命令行相关的Python语言特性

3.1.1 使用sys.argv获取命令行参数

编写Linux下的命令行工具,很多时候都需要解析命令行的参数。如果参数很简单,则可以不使用解析参数的库,直接访问命令行参数。在Python中,sys库下有一个名为argv的列表,该列表保存了所有的命令行参数。argv列表中第一个元素是命令行晨星的名称,其余的命令行参数以字符串的形式保存在该列表中。
例如,现在有一个名为test_argv.py的Python文件,该文件仅仅是导入sys库,然后使用print函数打印argv列表中的内容。test_argv.py的文件内容如下:

from __future__ import print_function
import sys

print(sys.argv)

下面是一个或者命令行参数,判断文件是否存在,如果存在判断其是否可读的示例:

#!/usr/bin/python


from __future__ import print_function
import sys
import os

def main():
    sys.argv.append("")
    filename = sys.argv[1]
    if not os.path.isfile(filename):
        raise SystemExit(filename + ' does not exists')
    elif not os.access(filename,os.R_OK):
        raise SystemExit(filename + ' is not accessible')
    else:
        print(filename, ' is accessible')

if __name__ == '__main__':
    main()

3.1.2 使用sys.stdin 和fileinput读取标准输入

众所周知,Shell能够通过管道把多个命令组合在一起来实现一个复杂的功能。因此,我们也希望在Python语言中使用管道来结合Python语言和Shell脚本的优势。
在Python标准库的sys库中,有三个文件描述符,分别是stdin、stdout和stderr,我们不需要调用open函数打开这几个文件就可以直接使用。例如,我们有一个名为read_stdin.py的文件,该文件仅仅是从标准输入中读取内容,然后打印到命令行终端。文件内容如下:

from __future__ import print_function
import sys

for line in sys.stdin:
    print(line, end="")

接下来,我们就可以想shell脚本一样,通过标准输入给该程序输入内容,如下所示:

cat /etc/passwd |python read_stdin.py

sys.stdin是一个普通的文件对象,该对象还有一些方法,通过这些方法我们可以进一步去处理标准输入中的内容。如通过readlines函数将标准输入的内容读取到一个例表中。

如果我们可以使用fileinput来进行多文件的处理。fileinput是Python语言的一个标准库,它提供了比sys,stdin更加通用的功能。使用fileinput,可以依次读取命令行参数中给出的多个文件。也就是说,fileinput会遍历sys.argv[1:]列表,并按行一次读取列表中的文件。如果列表为空,则fileinput默认读取标准输入中的内容。示例同上:

from __future__ import print_function
import fileinput

for line in fileinput.input():
    print(line, end="")

fileinput读取内容比sys.stdin更加灵活,看示例

cat /etc/passwd |python read_from_fileinput.py
python read_from_fileinput.py < /etc/passwd
python read_from_fileinput.py  /etc/passwd /etc/hosts

因为fileinput可以读取多个文件的内容,所以,fileinput提供了一些方法让我们知道当前读取的内容属于哪个文件。fileinput中常用的方法有:

  • filename: 当前正在读取的文件名
  • fileno: 文件的描述符
  • filelineno: 正在读取的行是当前文件的第几行
  • isfirstline: 正在读取的行是否为当前文件的第一行
  • isstdin fileinput: 正在读取文件还是直接从标准输入读取内容。

这些方法的使用也非常简单,看示例:

from __future__ import print_function
import fileinput

for line in fileinput.input():
    meta = [fileinput.filename(),fileinput.fileno(),fileinput.filelineno(),
            fileinput.isfirstline(),fileinput.isstdin()]
    print(*meta,end="")
    print(line,end="")

3.1.3 使用SystemExit异常打印错误信息

前面介绍了标准输入,如果我们要输出标准输出和错误输出的,可以使用sys.stdout.write方法和sys.stderr.write方法。如果我们的程序执行失败了,通常情况下我们会需要在标准版错误中输出错误信息,然后以非零的返回码退出程序。示例如下:

import sys

sys.stderr.write('error message')
sys.exit(1)

3.1.4 使用getpass库读取密码

getpass是一个非常简单的Python标准库,主要包含getuser函数和getpass函数。前者用来从环境变量中获取用户名,后者用来等待用户输入密码。getpass函数和input函数的区别在于,它不会将我们输入的密码显示在命令行中,从而避免我们输入的密码被他们看到。如下所示:

from __future__ import print_function
import getpass

user=getpass.getuser()
passwd=getpass.getpass('your password: ')
print(user,passwd)

3.2 使用ConfigParse解析配置文件

使用配置文件配置参数是很常见的需求,因此,各个语言都提供了相应的模块来解析配置文件。在Python语言中,标准库的ConfigParser模块用以解析配置文件。ConfigParser模块中包含了一个ConfigParser类,一个ConfigParser对象可以同时解析多个配置文件,一般情况下,我们只会使用ConfigParser解析一个配置文件。ConfigParser类提供了很多方法,我们可以使用这些方法解析、读取和修改配置文件。
要解析一个配置文件,首先要创建一个ConfigParser对象。创建ConfigParser时有多个参数,其中,比较重要的是allow_no_value。allow_no_value默认取值为False,表示在配置文件中不允许没有值的情况。但是有一些特殊环境,会有这种情况,如mysql的配置文件。

有了ConfigParser对象以后,可以使用read方法从配置文件中读取配置内容,也可以使用readfp方法从一个已经打开的文件中读取配置内容。

from __future__ import print_function
import ConfigParser

cf=ConfigParser.ConfigParser(allow_no_value=True)
cf.read('my.cnf')

ConfigParser中有很多方法,其中与读取配置文件,判断配置相关的方法有:

  • sections: 返回一个包含所有章节的列表
  • has_section: 判断章节是否存在
  • items:以元祖的形式返回所有选项的列表
  • options: 返回一个包含章节下所有选项的列表;
  • has_option: 判断某个选项是否存在
  • get、getboolean、getini、getfload: 判断选项的值

以上面打开的配置文件为例

In [5]: cf.sections()
Out[5]: ['client', 'mysql', 'mysqld', 'mysqldump']

In [6]: cf.has_section('client')
Out[6]: True

In [7]: cf.options('client')
Out[7]: ['port', 'socket']

In [8]: cf.has_option('client','port')
Out[8]: True

In [9]: cf.get('client','port')
Out[9]: '3306'

In [10]: cf.getint('client','port')
Out[10]: 3306

ConfigParser提供了很多方法便于我们修改配置文件。如下:

  • remove_section: 删除一个章节
  • add_section: 添加一个章节
  • remove_option: 删除一个选项
  • set: 添加一个选项
  • write: 将ConfigParser对象中的数据保存到文件中。

示例略过。

3.3 使用argparse解析命令行参数

对于命令行工具来说,命令行参数比陪你文件的使用更加广泛。在Python中,agrparse是标准库中用来解析命令行参数的模块,用来替代已经过时的optparse模块。argparse能够根据程序中的定义从sys.argv中解析出这些参数,并自动生成帮助和使用信息。

3.3.1 ArgumentParse解析器

使用argparse解析命令行参数时,首先需要创建一个解析器,创建方式如下所示:

import argparse
parser=argparse.ArgumentParser()

ArgumentParse类的初始化函数有多个参数,其中比较常用的是description。description是程序的描述信息,即帮助信息前的文字。参数内容先略过,下面是一个例子。

from __future__ import print_function
import argparse

def _argparse():
    parser = argparse.ArgumentParser(description="This is description")
    parser.add_argument('--host', action='store',
            dest='server',default="localhost", help='connect to host')
    parser.add_argument('-t', action='store_true',
            default=False, dest='boolean_switch', help='Set a switch to true')
    return parser.parse_args()

def main():
    parser = _argparse()
    print(parser)
    print('host =', parser.server)
    print('boolean_switch=', parser.boolean_switch)

if __name__ == '__main__':
    main()

由于我们为所有的选项都提供了默认值,因此,即使不传递任何参数也不会出错,如下:

[pangcm@blog_vm py_script]$ python test_argparse.py 
Namespace(boolean_switch=False, server='localhost')
host = localhost
boolean_switch= False
[pangcm@blog_vm py_script]$ python test_argparse.py  --host=127.0.0.1 -t
Namespace(boolean_switch=True, server='127.0.0.1')
host = 127.0.0.1
boolean_switch= True

使用argparse进行参数解析还有一个好处就是,它会自动生成帮助信息,如下:

[pangcm@blog_vm py_script]$ python test_argparse.py  --help
usage: test_argparse.py [-h] [--host SERVER] [-t]

This is description

optional arguments:
  -h, --help     show this help message and exit
  --host SERVER  connect to host
  -t             Set a switch to true

3.3.2 模仿MySQL客户端的命令行参数

from __future__ import print_function
import argparse

def _argparse():
    parser = argparse.ArgumentParser(description='A Python-MySQL client')
    parser.add_argument('--host', action='store', dest='host',
            required=True, help='connect to host')
    parser.add_argument('-u', '--user', action='store', dest='user',
            required=True, help='user for login')
    parser.add_argument('-p', '--password', action='store',
            dest='password',required=True, help='password to use when connecting to server')
    parser.add_argument('-P', '--port', action='store', dest='port',
            default=3306, type=int, help='port number to use for connection or 3306 for default')
    parser.add_argument('-v', '--version', action='version', version='%(prog)s 0.1')
    return parser.parse_args()

def main():
    parser = _argparse()
    conn_args = dict(host=parser.host, user=parser.user,
            password=parser.password, port=parser.port)
    print(conn_args)

if __name__ == '__main__':
    main()

3.4 使用loggin记录日志

对比自己写print函数打印程序的中间结果,使用标准库的日志模块有很多好处,包括:

  • 所有日志具有统一的格式,便于后续处理
  • 丰富的日志格式,只需要通过配置文件就可以修改日志的格式,不需要修改代码
  • 根据重要性对日志进行分类,可以只显示重要的日志
  • 自动管理日志文件,如按天切换一个新的文件,只保留一个月的日志文件等。

3.4.1 日志的作用

重要到不用说吧,比如诊断日志,排查问题;审计日志,为商业行为分析日志,如pv。

3.4.2 Python的logging模块

在最简单的使用中,我们直接导入logging模块,然后调用它的debug、info、warn、error、critical等函数记录日志。默认情况下,logging模块将日志打印到屏幕终端,日志级别为WARNING,也就是说,只有日志级别比WARNING高的日志才会被显示,如下所示:

#!/usr/bin/python

import logging

logging.debug('debug message')
logging.info('info message')
logging.warn('warn message')
logging.error('error message')
logging.critical('critical message')

日志的级别是一个逻辑上的概念,用来区分日志的重要程度。将日志分为不同的级别后,一方面可以在大多数时间只保存级别比较高的日志来提供性能;另一方面也便于日志的分析。
在Python的logging模块中,分为5分级别,其含义为:

日志级别 权重 含义
CRITICAL 50 严重错误,表明程序已经不能继续运行了
ERROR 40 发生了严重错误,必须马上处理
WARNING 30 应用程序可以容忍这些信息,软件可以正常运行,不过他们应该被检查及修复,否则将在不久的将来发生问题
INFO 20 证明事情按预期工作,突出强调应用程序的运行过程
DEBUG 10 详细信息,只有开发人员调试程序时才需要关注的事情

3.4.3 配置日志格式

在使用logging记录日志之前,我们可以进行一些简单的配置,如下:

#!/usr/bin/python

import logging

logging.basicConfig(filename='app.log',level=logging.INFO)

logging.debug('debug message')
logging.info('info message')
logging.warn('warn message')
logging.error('error message')
logging.critical('critical message')

执行上面的程序,会在当前目录下产生一个app.log文件。该文件存在INFO及INFO以上级别的日志。
尅看到,我们可以通过basicConfig方法对日志进行简单的配置,我们也可以进行更加复杂的日志配置。在这之前,需要先了解logging模块中的几个概念,即Logger、Handler及Formatter。

  • Logger: 日志记录器,是应用程序中能直接使用的接口
  • Handler: 日志处理器,用以表名将日志保存到什么地方以及保存多久
  • Formatter: 格式化,用以配置日志输出的格式。

对于比较简单的脚本,可以直接使用basicConfig在代码中配置日志。对于比较复杂的项目,可以将日志的配置保存到一个配置文件中,然后再代码中使用fileConfig函数读取配置文件。

下面是一个Python源码中配置日志的例子。

#!/usr/bin/python

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s : %(levelname)s : %(message)s',
    filename='app.log')

logging.debug('debug message')
...
logging.critical('critical message')

对于复杂的项目,一般将日志配置保存在配置文件中,如下:

[loggers]
keys = root

[handlers]
keys = logfile

[formatters]
keys = generic

[logger_root]
handlers = logfile

[handler_logfile]
class = handlers.TimedRotatingFileHandler
args = ('app.log', 'midnight', 1, 10)
level = DEBUG
formatter = generic

[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s] %(message)s

在这个日志配置文件中,我们首先在[loggers]中声明一个名为root的logger,在[handlers]中声明一个名为logfile的handler,并在[formatters]中声明一个名为generic的formatter。然后,我们在[logger_root]中定义root这个logger所使用的handler,在[handler_logfile]中定义了handler输出日志的方式、日志文件的切换时间等。最后,在[formatter_generic]中定义了日志的格式,包括日志产生的时间、日志的级别、产生日志的文件名和行号等信息。

有了配置文件以后,在Python代码中使用logging.config模块的fileConfig函数加载日志配置,如下所示:

import logging
import logging.config

logging.config.fileConfig('logging.cnf')

logging.debug('debug message')
...
logging.critical('critical message')

3.5 与命令行相关的开源项目

3.5.1 使用click解析命令行参数

Click是Flask的作者开发的一个第三方模块,用于快速创建命令行。它的作用与Python标准库的argprese相同,但是,使用更加简单。Click相对于标准库的argparse,就好比requests相对于标准库的urllib.
click是一个第三方库,首先要先安装才能使用。

pip install click

Click对argparse的主要改进在易用性,使用Click分为两个步骤:

  1. 使用@click.command()装饰一个函数,使之成为命令行接口;
  2. 使用@click.option()等装饰函数,为其添加命令行选项等。

示例如下:

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

在上面的例子中,函数hello接受两个参数,分别是count和name,它们的取值从命令行中获取。在这段程序中,我们使用了click模块中的command、option和echo,他们的作用如下:

  • command:使函数hello成为命令行接口
  • option: 增加命令行选项
  • echo: 输出结果,使用echo进行输出是为了获得更好的兼容性,因为Python2和Python3的print选项不是同一个东西来的。

运行上面的程序,可以通过命令行指定count和name的取值。由于我们在option函数中使用了prompt选项,因此,当我们没有直接指定name这个参数的时候,Click会提示我们在交互模式下输入,如下所示:

python hello.py --count=3

至于Click如何实现这些功能的,略过。

3.5.2 使用 prompt_toolkit 打造交互式命令行工具

如果你要打造一个用户体验良好的交互式命令行程序,那么可以了解prompt_toolkit的特性。使用prompt_toolkit能够支持语法高亮、支持代码补全可以使用Vi风格的快捷键等特性。我这里略过,有需要的百度去了解。

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

推荐阅读更多精彩内容