3.1 與命令行相關的Python語言特性
3.1.1 使用sys.argv獲取命令行參數(shù)
編寫Linux下的命令行工具,很多時候都需要解析命令行的參數(shù)。如果參數(shù)很簡單,則可以不使用解析參數(shù)的庫,直接訪問命令行參數(shù)。在Python中,sys庫下有一個名為argv的列表,該列表保存了所有的命令行參數(shù)。argv列表中第一個元素是命令行晨星的名稱,其余的命令行參數(shù)以字符串的形式保存在該列表中。
例如,現(xiàn)在有一個名為test_argv.py的Python文件,該文件僅僅是導入sys庫,然后使用print函數(shù)打印argv列表中的內(nèi)容。test_argv.py的文件內(nèi)容如下:
from __future__ import print_function
import sys
print(sys.argv)
下面是一個或者命令行參數(shù),判斷文件是否存在,如果存在判斷其是否可讀的示例:
#!/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能夠通過管道把多個命令組合在一起來實現(xiàn)一個復雜的功能。因此,我們也希望在Python語言中使用管道來結合Python語言和Shell腳本的優(yōu)勢。
在Python標準庫的sys庫中,有三個文件描述符,分別是stdin、stdout和stderr,我們不需要調(diào)用open函數(shù)打開這幾個文件就可以直接使用。例如,我們有一個名為read_stdin.py的文件,該文件僅僅是從標準輸入中讀取內(nèi)容,然后打印到命令行終端。文件內(nèi)容如下:
from __future__ import print_function
import sys
for line in sys.stdin:
print(line, end="")
接下來,我們就可以想shell腳本一樣,通過標準輸入給該程序輸入內(nèi)容,如下所示:
cat /etc/passwd |python read_stdin.py
sys.stdin是一個普通的文件對象,該對象還有一些方法,通過這些方法我們可以進一步去處理標準輸入中的內(nèi)容。如通過readlines函數(shù)將標準輸入的內(nèi)容讀取到一個例表中。
如果我們可以使用fileinput來進行多文件的處理。fileinput是Python語言的一個標準庫,它提供了比sys,stdin更加通用的功能。使用fileinput,可以依次讀取命令行參數(shù)中給出的多個文件。也就是說,fileinput會遍歷sys.argv[1:]列表,并按行一次讀取列表中的文件。如果列表為空,則fileinput默認讀取標準輸入中的內(nèi)容。示例同上:
from __future__ import print_function
import fileinput
for line in fileinput.input():
print(line, end="")
fileinput讀取內(nèi)容比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可以讀取多個文件的內(nèi)容,所以,fileinput提供了一些方法讓我們知道當前讀取的內(nèi)容屬于哪個文件。fileinput中常用的方法有:
- filename: 當前正在讀取的文件名
- fileno: 文件的描述符
- filelineno: 正在讀取的行是當前文件的第幾行
- isfirstline: 正在讀取的行是否為當前文件的第一行
- isstdin fileinput: 正在讀取文件還是直接從標準輸入讀取內(nèi)容。
這些方法的使用也非常簡單,看示例:
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方法。如果我們的程序執(zhí)行失敗了,通常情況下我們會需要在標準版錯誤中輸出錯誤信息,然后以非零的返回碼退出程序。示例如下:
import sys
sys.stderr.write('error message')
sys.exit(1)
3.1.4 使用getpass庫讀取密碼
getpass是一個非常簡單的Python標準庫,主要包含getuser函數(shù)和getpass函數(shù)。前者用來從環(huán)境變量中獲取用戶名,后者用來等待用戶輸入密碼。getpass函數(shù)和input函數(shù)的區(qū)別在于,它不會將我們輸入的密碼顯示在命令行中,從而避免我們輸入的密碼被他們看到。如下所示:
from __future__ import print_function
import getpass
user=getpass.getuser()
passwd=getpass.getpass('your password: ')
print(user,passwd)
3.2 使用ConfigParse解析配置文件
使用配置文件配置參數(shù)是很常見的需求,因此,各個語言都提供了相應的模塊來解析配置文件。在Python語言中,標準庫的ConfigParser模塊用以解析配置文件。ConfigParser模塊中包含了一個ConfigParser類,一個ConfigParser對象可以同時解析多個配置文件,一般情況下,我們只會使用ConfigParser解析一個配置文件。ConfigParser類提供了很多方法,我們可以使用這些方法解析、讀取和修改配置文件。
要解析一個配置文件,首先要創(chuàng)建一個ConfigParser對象。創(chuàng)建ConfigParser時有多個參數(shù),其中,比較重要的是allow_no_value。allow_no_value默認取值為False,表示在配置文件中不允許沒有值的情況。但是有一些特殊環(huán)境,會有這種情況,如mysql的配置文件。
有了ConfigParser對象以后,可以使用read方法從配置文件中讀取配置內(nèi)容,也可以使用readfp方法從一個已經(jīng)打開的文件中讀取配置內(nèi)容。
from __future__ import print_function
import ConfigParser
cf=ConfigParser.ConfigParser(allow_no_value=True)
cf.read('my.cnf')
ConfigParser中有很多方法,其中與讀取配置文件,判斷配置相關的方法有:
- sections: 返回一個包含所有章節(jié)的列表
- has_section: 判斷章節(jié)是否存在
- items:以元祖的形式返回所有選項的列表
- options: 返回一個包含章節(jié)下所有選項的列表;
- 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: 刪除一個章節(jié)
- add_section: 添加一個章節(jié)
- remove_option: 刪除一個選項
- set: 添加一個選項
- write: 將ConfigParser對象中的數(shù)據(jù)保存到文件中。
示例略過。
3.3 使用argparse解析命令行參數(shù)
對于命令行工具來說,命令行參數(shù)比陪你文件的使用更加廣泛。在Python中,agrparse是標準庫中用來解析命令行參數(shù)的模塊,用來替代已經(jīng)過時的optparse模塊。argparse能夠根據(jù)程序中的定義從sys.argv中解析出這些參數(shù),并自動生成幫助和使用信息。
3.3.1 ArgumentParse解析器
使用argparse解析命令行參數(shù)時,首先需要創(chuàng)建一個解析器,創(chuàng)建方式如下所示:
import argparse
parser=argparse.ArgumentParser()
ArgumentParse類的初始化函數(shù)有多個參數(shù),其中比較常用的是description。description是程序的描述信息,即幫助信息前的文字。參數(shù)內(nèi)容先略過,下面是一個例子。
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()
由于我們?yōu)樗械倪x項都提供了默認值,因此,即使不傳遞任何參數(shù)也不會出錯,如下:
[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進行參數(shù)解析還有一個好處就是,它會自動生成幫助信息,如下:
[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客戶端的命令行參數(shù)
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函數(shù)打印程序的中間結果,使用標準庫的日志模塊有很多好處,包括:
- 所有日志具有統(tǒng)一的格式,便于后續(xù)處理
- 豐富的日志格式,只需要通過配置文件就可以修改日志的格式,不需要修改代碼
- 根據(jù)重要性對日志進行分類,可以只顯示重要的日志
- 自動管理日志文件,如按天切換一個新的文件,只保留一個月的日志文件等。
3.4.1 日志的作用
重要到不用說吧,比如診斷日志,排查問題;審計日志,為商業(yè)行為分析日志,如pv。
3.4.2 Python的logging模塊
在最簡單的使用中,我們直接導入logging模塊,然后調(diào)用它的debug、info、warn、error、critical等函數(shù)記錄日志。默認情況下,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')
日志的級別是一個邏輯上的概念,用來區(qū)分日志的重要程度。將日志分為不同的級別后,一方面可以在大多數(shù)時間只保存級別比較高的日志來提供性能;另一方面也便于日志的分析。
在Python的logging模塊中,分為5分級別,其含義為:
| 日志級別 | 權重 | 含義 |
|---|---|---|
| CRITICAL | 50 | 嚴重錯誤,表明程序已經(jīng)不能繼續(xù)運行了 |
| ERROR | 40 | 發(fā)生了嚴重錯誤,必須馬上處理 |
| WARNING | 30 | 應用程序可以容忍這些信息,軟件可以正常運行,不過他們應該被檢查及修復,否則將在不久的將來發(fā)生問題 |
| INFO | 20 | 證明事情按預期工作,突出強調(diào)應用程序的運行過程 |
| DEBUG | 10 | 詳細信息,只有開發(fā)人員調(diào)試程序時才需要關注的事情 |
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')
執(zhí)行上面的程序,會在當前目錄下產(chǎn)生一個app.log文件。該文件存在INFO及INFO以上級別的日志。
尅看到,我們可以通過basicConfig方法對日志進行簡單的配置,我們也可以進行更加復雜的日志配置。在這之前,需要先了解logging模塊中的幾個概念,即Logger、Handler及Formatter。
- Logger: 日志記錄器,是應用程序中能直接使用的接口
- Handler: 日志處理器,用以表名將日志保存到什么地方以及保存多久
- Formatter: 格式化,用以配置日志輸出的格式。
對于比較簡單的腳本,可以直接使用basicConfig在代碼中配置日志。對于比較復雜的項目,可以將日志的配置保存到一個配置文件中,然后再代碼中使用fileConfig函數(shù)讀取配置文件。
下面是一個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]中定義了日志的格式,包括日志產(chǎn)生的時間、日志的級別、產(chǎn)生日志的文件名和行號等信息。
有了配置文件以后,在Python代碼中使用logging.config模塊的fileConfig函數(shù)加載日志配置,如下所示:
import logging
import logging.config
logging.config.fileConfig('logging.cnf')
logging.debug('debug message')
...
logging.critical('critical message')
3.5 與命令行相關的開源項目
3.5.1 使用click解析命令行參數(shù)
Click是Flask的作者開發(fā)的一個第三方模塊,用于快速創(chuàng)建命令行。它的作用與Python標準庫的argprese相同,但是,使用更加簡單。Click相對于標準庫的argparse,就好比requests相對于標準庫的urllib.
click是一個第三方庫,首先要先安裝才能使用。
pip install click
Click對argparse的主要改進在易用性,使用Click分為兩個步驟:
- 使用@click.command()裝飾一個函數(shù),使之成為命令行接口;
- 使用@click.option()等裝飾函數(shù),為其添加命令行選項等。
示例如下:
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()
在上面的例子中,函數(shù)hello接受兩個參數(shù),分別是count和name,它們的取值從命令行中獲取。在這段程序中,我們使用了click模塊中的command、option和echo,他們的作用如下:
- command:使函數(shù)hello成為命令行接口
- option: 增加命令行選項
- echo: 輸出結果,使用echo進行輸出是為了獲得更好的兼容性,因為Python2和Python3的print選項不是同一個東西來的。
運行上面的程序,可以通過命令行指定count和name的取值。由于我們在option函數(shù)中使用了prompt選項,因此,當我們沒有直接指定name這個參數(shù)的時候,Click會提示我們在交互模式下輸入,如下所示:
python hello.py --count=3
至于Click如何實現(xiàn)這些功能的,略過。
3.5.2 使用 prompt_toolkit 打造交互式命令行工具
如果你要打造一個用戶體驗良好的交互式命令行程序,那么可以了解prompt_toolkit的特性。使用prompt_toolkit能夠支持語法高亮、支持代碼補全可以使用Vi風格的快捷鍵等特性。我這里略過,有需要的百度去了解。