用Python寫CLI:參數解析

對CLI程序來說,參數解析大概是一個首要的問題。

當然,也有例外。

無參數腳本

許多常用命令,不需要輸入參數,就可以按照我們的預想去執行,比如ls

以Python的Helloworld為例,它就是一個無參數腳本。

print('Hello world!')

這個腳本的作用很明確,就是打印Hello world!字樣到sys.stdout。默認情況下,也就是Terminal的回顯中。它不需要任何參數。

無參數腳本雖然使用方便,但是通用性差。沒有參數,是因為執行內容與環境高度依賴,或者一些可以成為參數的變量被寫死。這樣的腳本,往往只是一次性用品,或者常用工具的雛形。

單個參數腳本

如果我們希望傳入單個參數,那么也比較簡單。

比如,在Helloworld的基礎上,我們增加一個參數,讓腳本打印我們傳入的參數。腳本的名稱就叫echo.py

import sys

print(sys.argv[1])

如果我們執行python echo.py hello,就會打印出hello

sys.argv是一個保存命令行參數的列表,而其中用[1]索引到的的第二個元素,就是我們輸入的那個參數hello

  • sys.argv
    The list of command line arguments passed to a Python script. argv[0] is the script name (it is operating system dependent whether this is a full pathname or not). If the command was executed using the -c command line option to the interpreter, argv[0] is set to the string '-c'. If no script name was passed to the Python interpreter, argv[0] is the empty string.

如果打印整個列表,改為print(sys.argv),會更明白它的涵義。

$ python echo.py hello
['echo.py', 'hello']

$ ./echo.py hello world
['./echo.py', 'hello', 'world']

似乎,這個東西也能支持多個命令行參數?且慢,我們之前的腳本還有bug呢!

假如我不輸入任何參數,結果會如何?

$ python echo.py
Traceback (most recent call last):
  File "echo.py", line 3, in <module>
    print(sys.argv[1])
IndexError: list index out of range

沒錯,打印之前,需要做長度檢查,echo.py需要修改。

import sys

if len(sys.argv) > 1:
    print(sys.argv[1])

這樣,一個單參數的腳本,總算是沒問題了。至于多個參數,別想了。

這種獲取參數的方法非常原始,與shell的$1類似。它難以支持多個參數而無隱患,更難以進行復雜的參數解析。

想想類似cp這種命令怎么做?

$ cp file0 file1
$ cp -r dir0 dir1
$ cp dir1 dir2 -r

多個參數解析

很多Python腳本的參數解析,還在使用optparse。我建議新腳本就別用它了,因為官網文檔也這么說。

Python 2:

New in version 2.3, Deprecated since version 2.7

Python 3:

Deprecated since version 3.2: The optparse module is deprecated and will not be developed further; development will continue with the argparse module.

相比argparse來說,optparse功能略弱,并且不再維護了。

另外,還有一些更老的腳本,使用C風格的getopt。這雖然沒有被標為廢棄,但是也不推薦新項目、新用戶使用了。

Note:
The getopt module is a parser for command line options whose API is designed to be familiar to users of the C getopt() function. Users who are unfamiliar with the C getopt() function or who would like to write less code and get better help and error messages should consider using the argparse module instead.

sys.argv,到getopt,再到optparse,最后到argparse,在參數解析的技術上實現了三次跨越。第一次使模糊的解析變得清晰,使得孤立的參數變得結構化;第二次讓繁瑣的解析變得簡單,讓幫助文檔與參數組織在一起。第三次則自動生成幫助文檔與錯誤提示,并且支持形如git的子命令。

注意:argparse僅在Python 2.7+與Python 3.3+的版本自帶。

下面以argparse為例,介紹各種形式的參數解析。

無參數

一個沒有參數的參數解析,應該最適合理解這個模塊的用法。

import argparse


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.parse_args()
    print("Hello world!")

執行這個helloworld.py文件,看看結果。

$ python helloworld.py 
Hello world!

似乎什么也沒發生。那么,加個-h試試?

$ python helloworld.py -h
usage: helloworld.py [-h]

optional arguments:
  -h, --help  show this help message and exit

哇!一個沒有任何幫助的幫助文檔,就這樣自動生成了。

-h--help被默認占用,顯示幫助文檔并退出。可以看到,Hello world!字樣,并未在幫助信息的前后顯示。

真正的參數解析,其實就是在parse_args()前,對argparse.ArgumentParser()進行一些設置。

位置參數

為了展示位置參數(Positional arguments),下面寫一個cp.py,實現簡單的文件復制功能。

import argparse
import shutil


def _parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "source",
        help="specify the file to be copied",
    )
    parser.add_argument(
        "target",
        help="specify a path to copy to",
    )
    return parser.parse_args()


if __name__ == '__main__':
    args = _parse_args()
    shutil.copy(src=args.source, dst=args.target)

cp.py命令后,第一個參數被識別為source,第二個參數被識別為target,然后執行復制。在經歷parse_args()后,sys.argv的參數列表,變成了結構化的args

args的類型,是一個<class 'argparse.Namespace'>。)

如果執行python cp.py cp.py cp2.py,那么不會有任何顯示信息,成功執行復制操作。

如果多一個或者少一個參數呢?

$ python cp.py cp.py
usage: cp.py [-h] source target
cp.py: error: too few arguments
$ python cp.py cp.py cp2.py cp3.py
usage: cp.py [-h] source target
cp.py: error: unrecognized arguments: cp3.py

這就比直接使用sys.argv的可靠性要高多了。

幫助文檔

讓我們看看前面那個腳本的幫助文檔:

$ python cp.py -h
usage: cp.py [-h] source target

positional arguments:
  source      specify the file to be copied
  target      specify a path to copy to

optional arguments:
  -h, --help  show this help message and exit

只是寫了兩句help='...'而已,竟然生成了這么有條理的幫助信息!是不是心中充滿感動,有一種活在21世紀的感覺?

可選參數

位置參數如果過多,含義往往過于模糊。對參數比較復雜的CLI程序,可以使用多個可選參數(Optional arguments)來指定。

比如,寫一個增強型的echo.py,使其支持一個--by參數,指定發言人。

def _read_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'words',
        nargs='*',
        help='the words to be print',
    )
    parser.add_argument(
        '-b', '--by',
        default=None,
        help='who says the words',
        metavar='speaker',
    )
    parser.add_argument(
        '-v', '--version',
        action='version',
        version='%(prog)s 1.0.0',
    )
    return parser.parse_args()


if __name__ == '__main__':
    args = _read_args()

    words = ' '.join(args.words)
    if args.by is not None:
        words = '%s: %s' % (args.by, words)
    print(words)

參數-b--by,在解析后可以用args.by來調用。如果用args.b,則會報錯,因為在長短參數都具備的情況下,優先使用長參數;在只有短參數的情況下,才會使用短參數,args.b才存在。

另外,也支持形如--long-name的長參數。在解析后,用args.long_name來調用,減號-換成了下劃線_

以下為執行與回顯。

$ python echo.py -h
usage: echo.py [-h] [-v] [-b speaker] [words [words ...]]

positional arguments:
  words                 the words to be print

optional arguments:
  -h, --help            show this help message and exit
  -v, --version         show program's version number and exit
  -b speaker, --by speaker
                        who says the words
$ python echo.py -v
echo.py 1.0.0
$ python echo.py How are you?
How are you?
$ python echo.py I am fine, thank you. --by me
me: I am fine, thank you.

可選參數是復雜CLI程序組織輸入的最佳選擇。在使用時可以隨意調換參數的輸入順序,也給出了更加明顯的使用提示。

add_argument() 的一些形參

前面echo.py的代碼中,add_argument()里有出現nargsdefaulthelp等形式參數,這些都是可選功能。

  • nargs='*',使得words可以接受一組不定長度的參數,作為一個list。
  • help='...',指定幫助提示信息。
  • default=None,如果該參數未指定,則使用默認值None
  • metavar='speaker',指定幫助信息里的顯示,否則默認為長參數的全大寫,如-b BY, --by BY who says the words
  • action='...',這是一個比較復雜的選項,詳見action
    其中,version='%(prog)s 1.0.0',與action='version'配套,顯示格式化的版本信息。
    %(prog),則是一個內置的字符串格式化變量,默認值為程序名,詳見prog

可以在官網文檔add_argument中查看到更多選項。

  • name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.
  • action - The basic type of action to be taken when this argument is encountered at the command line.
  • nargs - The number of command-line arguments that should be consumed.
  • const - A constant value required by some action and nargs selections.
  • default - The value produced if the argument is absent from the command line.
  • type - The type to which the command-line argument should be converted.
  • choices - A container of the allowable values for the argument.
  • required - Whether or not the command-line option may be omitted (optionals only).
  • help - A brief description of what the argument does.
  • metavar - A name for the argument in usage messages.
  • dest - The name of the attribute to be added to the object returned by parse_args().

子命令

如果CLI程序有多個相互獨立的功能,卻又需要組織在一起,那么可以使用子命令。最典型的子命令案例,就是Git。

下面展示一個仿冒版git.py腳本。

import argparse

import clone
import init


def _init_subparsers(parent):
    subparsers = parent.add_subparsers(title='sub commands')
    parser_clone = subparsers.add_parser(
        'clone',
        help='Clone a repository into a new directory'
    )
    clone.init_parser(parser_clone)  # add_argument() in module clone
    parser_clone.set_defaults(func=clone.main)
    parser_init = subparsers.add_parser(
        'init',
        help='Create an empty Git repository or reinitialize an existing one'
    )
    init.init_parser(parser_init)  # add_argument() in module init
    parser_init.set_defaults(func=init.main)


def _parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '-v', '--version',
        action='version',
        version='%(prog)s x.x.x',
    )

    _init_subparsers(parser)

    return parser.parse_args()


if __name__ == '__main__':
    args = _parse_args()
    args.func(args)

顯示一下版本與幫助文檔。

$ python git.py -v
git.py x.x.x
$ python git.py -h
usage: git.py [-h] [-v] {clone,init} ...

optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

sub commands:
  {clone,init}
    clone        Clone a repository into a new directory
    init         Create an empty Git repository or reinitialize an existing
                 one

通過add_subparsers(),可以獲得一個<class 'argparse._SubParsersAction'>。再執行add_parser,可以添加若干個子命令。

每一個子命令,都是一個<class 'argparse.ArgumentParser'>。所以,同樣支持位置參數、可選參數、子命令等。

clone.init_parser(parser_clone),是省略的子命令parser設置。它與當前文件的_parse_args()類似,都是對argparse.ArgumentParser的解析。

這里,通過parser.set_defaults(func=module.main)的方式,把func設置為不同module的函數入口(這里是main函數)。在參數解析完畢后,執行args.func(args),可以調用對應子命令指定的函數。并且,將自身作為參數傳入,可以獲得解析后的結構化參數。

比如,python git.py clone執行的就是clone.main(args),而python git.py init執行的則是init.main(args)

(還有另一種用法,是args.func(**vars(args))。指定的func那邊,可以直接在函數聲明中定義解析后的參數,不過需要過濾多余參數。)

對子命令的解析,也可以直接把subparsers傳進另一個模塊里去做自定義的init_parser_in(subparsers),完成add_parseradd_argumentset_defaults三步。這樣,把當前文件當成一個總入口,子命令都在獨立的module中,可以達到一定的模塊化效果。

也許,子命令最大的作用,是在顯示幫助文檔時,不會滾動多屏,嚇到使用者。

小結

在有了參數解析后,Python代碼就從普通腳本,升級成了CLI程序。

更詳細的內容,可以查看官方文檔argparse或教程tutorial

這是21世紀第一個十年的參數解析技術,秒殺一切上個世紀的殘留。作為Python的標準庫之一,它的適用范圍廣,解析功能多樣,效果穩定。我建議,參數解析技術還停留在上個世紀的Python開發者,可以學習使用它。

而在21世紀的第二個十年,則有另外三個流行的參數解析庫,或更方便、或更簡潔、或更有趣。有閑再說吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容