對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 Cgetopt()
function. Users who are unfamiliar with the Cgetopt()
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()
里有出現nargs
、default
、help
等形式參數,這些都是可選功能。
-
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_parser
、add_argument
、set_defaults
三步。這樣,把當前文件當成一個總入口,子命令都在獨立的module中,可以達到一定的模塊化效果。
也許,子命令最大的作用,是在顯示幫助文檔時,不會滾動多屏,嚇到使用者。
小結
在有了參數解析后,Python代碼就從普通腳本,升級成了CLI程序。
更詳細的內容,可以查看官方文檔argparse或教程tutorial。
這是21世紀第一個十年的參數解析技術,秒殺一切上個世紀的殘留。作為Python的標準庫之一,它的適用范圍廣,解析功能多樣,效果穩定。我建議,參數解析技術還停留在上個世紀的Python開發者,可以學習使用它。
而在21世紀的第二個十年,則有另外三個流行的參數解析庫,或更方便、或更簡潔、或更有趣。有閑再說吧。