用 Python 解析命令列引數

2020-06-05 23:16:00

借鑑 C 語言的歷史,學習如何用 Python 編寫有用的 CLI 程式。

本文的目標很簡單:幫助新的 Python 開發者瞭解一些關於命令列介面(CLI)的歷史和術語,並探討如何在 Python 中編寫這些有用的程式。

最初……

首先,從 Unix 的角度談談命令列介面設計。

Unix 是一種計算機作業系統,也是 Linux 和 macOS(以及許多其他作業系統)的祖先。在圖形化使用者介面之前,使用者通過命令列提示符與計算機進行互動(想想如今的 Bash 環境)。在 Unix 下開發這些程式的主要語言是 C,它的功能非常強大。

因此,我們至少應該了解 C 程式的基礎知識。

假設你沒有讀過上面那個連結的內容,C 程式的基本架構是一個叫做 main 的函數,它的簽名是這樣的。

   int main(int argc, char **argv)   {   ...   }

對於 Python 程式設計師來說,這應該不會顯得太奇怪。C 函數首先有一個返回型別、一個函數名,然後是括號內的型別化引數。最後,函數的主體位於大括號之間。函數名 main執行時連結器(構造和執行程式的程式)如何決定從哪裡開始執行你的程式。如果你寫了一個 C 程式,而它沒有包含一個名為 main 的函數,它將什麼也做不了。傷心。

函數引數變數 argcargv 共同描述了程式被呼叫時使用者在命令列輸入的字串列表。在典型的 Unix 命名傳統中,argc 的意思是“引數計數argument count”,argv 的意思是“引數向量argument vector”。向量聽起來比列表更酷,而 argl 聽起來就像一個要勒死的求救聲。我們是 Unix 系統的程式設計師,我們不求救。我們讓其他人哭著求救。

再進一步

$ ./myprog foo bar -x baz

如果 myprog 是用 C 語言實現的,則 argc 的值是 5,而 argv 是一個有五個條目的字元指標陣列。(不要擔心,如果這聽起來過於技術,那換句話說,這是一個由五個字串組成的列表。)向量中的第一個條目 argv[0] 是程式的名稱。argv 的其餘部分包含引數。

   argv[0] == "./myprog"   argv[1] == "foo"   argv[2] == "bar"   argv[3] == "-x"   argv[4] == "baz"      /* 註:不是有效的 C 程式碼 */

在 C 語言中,你有很多方法來處理 argv 中的字串。你可以手動地迴圈處理陣列 argv,並根據程式的需要解釋每個字串。這相對來說比較簡單,但會導致程式的介面大相逕庭,因為不同的程式設計師對什麼是“好”有不同的想法。

include <stdio.h>/* 一個列印 argv 內容的簡單 C 程式。 */int main(int argc, char **argv) {    int i;       for(i=0; i<argc; i++)      printf("%s\n", argv[i]);}

早期對命令列標準化的嘗試

命令列武器庫中的下一個武器是一個叫做 getoptC 標準庫函數。這個函數允許程式設計師解析開關,即前面帶破折號的引數(比如 -x),並且可以選擇將後續引數與它們的開關配對。想想 /bin/ls -alSh 這樣的命令呼叫,getopt 就是最初用來解析該引數串的函數。使用 getopt 使命令列的解析變得相當簡單,並改善了使用者體驗(UX)。

include <stdio.h>#include <getopt.h>#define OPTSTR "b:f:"extern char *optarg;int main(int argc, char **argv) {    int opt;    char *bar = NULL;    char *foo = NULL;       while((opt=getopt(argc, argv, OPTSTR)) != EOF)       switch(opt) {          case 'b':              bar = optarg;              break;          case 'f':              foo = optarg;              break;          case 'h':          default':              fprintf(stderr, "Huh? try again.");              exit(-1);              /* NOTREACHED */       }    printf("%s\n", foo ? foo : "Empty foo");    printf("%s\n", bar ? bar : "Empty bar");}

就個人而言,我希望 Python 有開關,但這永遠、永遠不會發生

GNU 時代

GNU 專案出現了,並為他們實現的傳統 Unix 命令列工具引入了更長的格式引數,比如--file-format foo。當然,我們這些 Unix 程式設計師很討厭這樣,因為打字太麻煩了,但是就像我們這些舊時代的恐龍一樣,我們輸了,因為使用者喜歡更長的選項。我從來沒有寫過任何使用 GNU 風格選項解析的程式碼,所以這裡沒有程式碼範例。

GNU 風格的引數也接受像 -f foo 這樣的短名,也必須支援。所有這些選擇都給程式設計師帶來了更多的工作量,因為他們只想知道使用者要求的是什麼,然後繼續進行下去。但使用者得到了更一致的使用者體驗:長格式選項、短格式選項和自動生成的幫助,使使用者不必再試圖閱讀臭名昭著的難以解析的手冊頁面(參見 ps 這個特別糟糕的例子)。

但我們正在討論 Python?

你現在已經接觸了足夠多(太多?)的命令列的歷史,對如何用我們最喜歡的語言來編寫 CLI 有了一些背景知識。Python 在命令列解析方面給出了類似的幾個選擇:自己解析,自給自足batteries-included的方式,以及大量的第三方方式。你選擇哪一種取決於你的特定情況和需求。

首先,自己解析

你可以從 sys 模組中獲取程式的引數。

import sysif __name__ == '__main__':   for value in sys.argv:       print(value)

自給自足

在 Python 標準庫中已經有幾個引數解析模組的實現:getoptoptparse,以及最近的 argparseargparse 允許程式設計師為使用者提供一致的、有幫助的使用者體驗,但就像它的 GNU 前輩一樣,它需要程式設計師做大量的工作和“模板程式碼”才能使它“奏效”。

from argparse import ArgumentParserif __name__ == "__main__":   argparser = ArgumentParser(description='My Cool Program')   argparser.add_argument("--foo", "-f", help="A user supplied foo")   argparser.add_argument("--bar", "-b", help="A user supplied bar")      results = argparser.parse_args()   print(results.foo, results.bar)

好處是當使用者呼叫 --help 時,有自動生成的幫助。但是自給自足batteries included的優勢呢?有時,你的專案情況決定了你對第三方庫的存取是有限的,或者說是沒有,你不得不用 Python 標準庫來“湊合”。

CLI 的現代方法

然後是 ClickClick 框架使用裝飾器的方式來構建命令列解析。突然間,寫一個豐富的命令列介面變得有趣而簡單。在裝飾器的酷炫和未來感的使用下,很多複雜的東西都消失了,使用者驚嘆於自動支援關鍵字補完以及上下文幫助。所有這些都比以前的解決方案寫的程式碼更少。任何時候,只要你能寫更少的程式碼,還能把事情做好,就是一種勝利。而我們都想要勝利。

import [email protected]()@click.option("-f", "--foo", default="foo", help="User supplied foo.")@click.option("-b", "--bar", default="bar", help="User supplied bar.")def echo(foo, bar):    """My Cool Program       It does stuff. Here is the documentation for it.    """    print(foo, bar)   if __name__ == "__main__":    echo()

你可以在 @click.option 裝飾器中看到一些與 argparse 相同的模板程式碼。但是建立和管理引數分析器的“工作”已經被抽象化了。現在,命令列引數被解析,而值被賦給函數引數,從而函數 echo魔法般地呼叫。

Click 介面中新增引數就像在堆疊中新增另一個裝飾符並將新的引數新增到函數定義中一樣簡單。

但是,等等,還有更多!

Typer 建立在 Click 之上,是一個更新的 CLI 框架,它結合了 Click 的功能和現代 Python 型別提示。使用 Click 的缺點之一是必須在函數中新增一堆裝飾符。CLI 引數必須在兩個地方指定:裝飾符和函數參數列。Typer 免去你造輪子 去寫 CLI 規範,讓程式碼更容易閱讀和維護。

import typercli = typer.Typer()@cli.command()def echo(foo: str = "foo", bar: str = "bar"):    """My Cool Program       It does stuff. Here is the documentation for it.    """    print(foo, bar)   if __name__ == "__main__":    cli()

是時候開始寫一些程式碼了

哪種方法是正確的?這取決於你的用例。你是在寫一個只有你才會使用的快速而粗略的指令碼嗎?直接使用 sys.argv 然後繼續編碼。你需要更強大的命令列解析嗎?也許 argparse 就夠了。你是否有很多子命令和複雜的選項,你的團隊是否會每天使用它?現在你一定要考慮一下 ClickTyper。作為一個程式設計師的樂趣之一就是魔改出替代實現,看看哪一個最適合你。

最後,在 Python 中有很多用於解析命令列引數的第三方軟體包。我只介紹了我喜歡或使用過的那些。你喜歡和/或使用不同的包是完全可以的,也是我們所期望的。我的建議是先從這些包開始,然後看看你最終的結果。

去寫一些很酷的東西吧。