用Python寫CLI:參數(shù)解析

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

當(dāng)然,也有例外。

無參數(shù)腳本

許多常用命令,不需要輸入?yún)?shù),就可以按照我們的預(yù)想去執(zhí)行,比如ls。

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

print('Hello world!')

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

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

單個參數(shù)腳本

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

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

import sys

print(sys.argv[1])

如果我們執(zhí)行python echo.py hello,就會打印出hello。

sys.argv是一個保存命令行參數(shù)的列表,而其中用[1]索引到的的第二個元素,就是我們輸入的那個參數(shù)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']

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

假如我不輸入任何參數(shù),結(jié)果會如何?

$ 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])

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

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

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

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

多個參數(shù)解析

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

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功能略弱,并且不再維護(hù)了。

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

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,在參數(shù)解析的技術(shù)上實現(xiàn)了三次跨越。第一次使模糊的解析變得清晰,使得孤立的參數(shù)變得結(jié)構(gòu)化;第二次讓繁瑣的解析變得簡單,讓幫助文檔與參數(shù)組織在一起。第三次則自動生成幫助文檔與錯誤提示,并且支持形如git的子命令。

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

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

無參數(shù)

一個沒有參數(shù)的參數(shù)解析,應(yīng)該最適合理解這個模塊的用法。

import argparse


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

執(zhí)行這個helloworld.py文件,看看結(jié)果。

$ python helloworld.py 
Hello world!

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

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

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

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

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

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

位置參數(shù)

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

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命令后,第一個參數(shù)被識別為source,第二個參數(shù)被識別為target,然后執(zhí)行復(fù)制。在經(jīng)歷parse_args()后,sys.argv的參數(shù)列表,變成了結(jié)構(gòu)化的args

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

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

如果多一個或者少一個參數(shù)呢?

$ 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世紀(jì)的感覺?

可選參數(shù)

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

比如,寫一個增強(qiáng)型的echo.py,使其支持一個--by參數(shù),指定發(fā)言人。

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)

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

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

以下為執(zhí)行與回顯。

$ 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.

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

add_argument() 的一些形參

前面echo.py的代碼中,add_argument()里有出現(xiàn)nargs、default、help等形式參數(shù),這些都是可選功能。

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

可以在官網(wǎng)文檔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程序有多個相互獨(dú)立的功能,卻又需要組織在一起,那么可以使用子命令。最典型的子命令案例,就是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'>。再執(zhí)行add_parser,可以添加若干個子命令。

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

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

這里,通過parser.set_defaults(func=module.main)的方式,把func設(shè)置為不同module的函數(shù)入口(這里是main函數(shù))。在參數(shù)解析完畢后,執(zhí)行args.func(args),可以調(diào)用對應(yīng)子命令指定的函數(shù)。并且,將自身作為參數(shù)傳入,可以獲得解析后的結(jié)構(gòu)化參數(shù)。

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

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

對子命令的解析,也可以直接把subparsers傳進(jìn)另一個模塊里去做自定義的init_parser_in(subparsers),完成add_parseradd_argument、set_defaults三步。這樣,把當(dāng)前文件當(dāng)成一個總?cè)肟?,子命令都在?dú)立的module中,可以達(dá)到一定的模塊化效果。

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

小結(jié)

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

更詳細(xì)的內(nèi)容,可以查看官方文檔argparse或教程tutorial。

這是21世紀(jì)第一個十年的參數(shù)解析技術(shù),秒殺一切上個世紀(jì)的殘留。作為Python的標(biāo)準(zhǔn)庫之一,它的適用范圍廣,解析功能多樣,效果穩(wěn)定。我建議,參數(shù)解析技術(shù)還停留在上個世紀(jì)的Python開發(fā)者,可以學(xué)習(xí)使用它。

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

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

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

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