第3章 打造命令行工具

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分為兩個步驟:

  1. 使用@click.command()裝飾一個函數(shù),使之成為命令行接口;
  2. 使用@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風格的快捷鍵等特性。我這里略過,有需要的百度去了解。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容