1. 程式人生 > >python製作命令列工具——fire

python製作命令列工具——fire

**前言** 本篇教程的目的是希望大家可以通讀完此篇之後,可以使用python製作一款符合自己需求的linux工具。 本教程使用的是google開源的python第三方庫:fire 無論是學生黨自己做著練手,還是工作中確有需求,本篇都儘可能通過簡單的例子來示範該第三方庫的用法,其中若有描述不當的地方,望留言指出。 ------ ### 一、快速介紹 來一波官方介紹。 > - Python Fire是一個庫,用於從任何Python物件自動生成命令列介面。 > - 是用python建立CLI的一種簡單方法。 > - 是開發和除錯Python程式碼的一個有用工具。 > - Python Fire幫助探索現有程式碼或將其他人的程式碼轉換為CLI。 > - 使得Bash和Python之間的轉換更加容易。 > - 通過使用已經匯入和建立的模組和變數來設定REPL, Python Fire使使用Python REPL變得更容易。 沒聽懂 **???** 不是太明白 **???** 不要緊,看完本篇就懂了。 ------ ### 二、快速安裝 - pip安裝:`pip install fire` - conda安裝:`conda install fire -c conda-forge` - 原始碼安裝: ``` 1. git clone https://github.com/google/python-fire.git 2. cd python-fire 3. python setup.py install ``` Github地址:[python-fire](https://github.com/google/python-fire.git) ------ ### 三、快速上手 實踐出真知 建立一個test.py檔案,寫入以下內容 ```python import fire def test(your_var="default_value"): return 'This is a test ! value : %s' % your_var if __name__ == '__main__': fire.Fire(test) ``` 咱們來看一下效果 ```shell # 預設引數 root@node:~# python test.py This is a test ! value : default_value # 關鍵字引數 root@node:~# python test.py --your_var=newValue This is a test ! value : newValue # 位置引數 root@node:~# python test.py localtionValue This is a test ! value : localtionValue ``` 現在呢,我們反過頭來看一下官方介紹的第一行: > Python Fire是一個庫,用於從任何Python物件自動生成命令列介面。 注意關鍵字:任何python物件。這意味著什麼? 我們來看一段程式碼: ``` import fire boy_name = 'XiaoMing' girl_name = 'XiaoHong' if __name__ == '__main__': fire.Fire() ``` 試一下:`python test.py boy_name` 是不是明白了些什麼。 聊完這**預設引數**、**關鍵字引數**、**位置引數**,當然不能少了 ***args** 和 ** **kwargs** . 還是來看程式碼示例: ```python import fire def run(*args): arg_list = list(args) return ' | '.join(arg_list) if __name__ == '__main__': fire.Fire(run) ``` 跑一下就懂啦 ``` root@node:~# python test.py run qwe rty uio asd fgh qwe | rty | uio | asd | fgh ``` 官方給的示例是這個樣子的~~~ ```python import fire def order_by_length(*items): """Orders items by length, breaking ties alphabetically.""" sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item))) return ' '.join(sorted_items) if __name__ == '__main__': fire.Fire(order_by_length) ``` 就是加了個長度和字母順序的排序,來跑一下,看一下效果: ```shell $ python example.py dog cat elephant cat dog elephant ``` 除此之外呢,我們還可以給輸出結果加點料,還是剛才我們寫的那個例子: ``` root@node:~# python test.py run qwe rty uio asd fgh - upper QWE | RTY | UIO | ASD | FGH ``` 在這裡,我們通過命令列對傳入的物件和呼叫結果執行相同的操作,譬如這裡的 `upper` 敲黑板劃重點:**分隔符 “ - ” 之後的所有引數都將用於處理函式的結果,而不是傳遞給函式本身。預設的分隔符是連字元 “ - ”。** 預設的分隔符也是可以改的,用到了`fire`的內建引數。 ``` root@node:~# python test.py run qwe rty uio asd fgh X upper -- --separator=X QWE | RTY | UIO | ASD | FGH ``` 其中的`separator`就是fire的一個內建引數,更多內建引數文末有提到。 我們再來看一下`fire`給我們提供的命令列傳參時,**資料的型別**。比較特殊的是,`fire`根據值決定型別。 ```python import fire fire.Fire(lambda obj: type(obj).__name__) ``` 如果有剛學python的小夥伴,記得一定要學一下`lambda`函式,在這裡我可以轉化為普通寫法。 ``` import fire def test(obj): return type(obj).__name__ if __name__ == '__main__': fire.Fire(test) ``` 通過簡單的一行程式碼來看一下各種資料型別如何通過命令列傳參: ```shell $ python example.py 10 int $ python example.py 10.0 float $ python example.py hello str $ python example.py '(1,2)' tuple $ python example.py [1,2] list $ python example.py True bool $ python example.py {name:David} dict ``` 但是當你想傳遞一個str型別的10,你就要注意了,看以下例子: ```shell $ python example.py 10 int $ python example.py "10" int $ python example.py '"10"' str $ python example.py "'10'" str $ python example.py \"10\" str ``` 我們可以看到,你雖然敲了`"10"`,但是依然被判定為`int`,bash會自動處理掉你引數的第一層引號。所以,如果想傳`str`型別的10,要再加一層引號,單雙引號分開用,或者把引號轉義。 如果要傳的是dict引數,那就更要小心謹慎了。 ```shell # 標準寫法 $ python example.py '{"name": "David Bieber"}' dict # 要這麼寫也沒啥問題 $ python example.py {"name":'"David Bieber"'} dict # 但要這麼寫就解析成了字串了 $ python example.py {"name":"David Bieber"} str # 再加個空格,字串都不是了 $ python example.py {"name": "David Bieber"} # Wrong. This isn't even treated as a single argument. ``` 到這裡,我想大家應該大概明白了 fire 的方便快捷之處。 到了這一步的時候,雖然實現了基本功能,但還是和平時我們使用的 linux 命令列工具有很大的區別: 1. 每次跑命令都要再敲一個python 2. 每次還要指向指定的py檔案或到指定的目錄下 首先說第一個問題,每次多敲六個字母和一個空格,作為一個linux命令列工具是非常不合格的,本來命令列工具就在追求簡單化,這種指定直譯器的操作我們當然要儘可能省掉咯 第二個問題,老是指定檔案的目錄就更麻煩了,日常使用的時候在不同的伺服器跑命令還要想想放在哪裡,而且如果使用絕對路徑的話,更會導致命令的冗長。 **下面我們來解決一下這兩個“小”問題:** 1. 在檔案的第一行指定python直譯器,這樣就無需在我們執行該檔案時再指定直譯器 ```python #!/usr/bin/python import fire def test(your_var="default_value"): return 'This is a test ! value : %s' % your_var if __name__ == '__main__': fire.Fire(test) ``` 2. 增加檔案的可執行許可權 ```shell root@node:~# chmod +x test.py ``` 3. 美化以下,去掉小尾巴(僅僅是給檔案改了個名字, 這一步非必須) ``` root@node:~# mv test.py mytool ``` 4. 做個軟連線,可以隨時隨地找得到該命令 ``` root@node:~# ln -s /root/mytool /usr/bin/mytool ``` ***附:如果需要指定編碼的話,可以在檔案頭部加一行,比如*** ```python #!/usr/bin/python # coding: utf-8 ``` 這個時候,我們隨意在伺服器的任意位置執行 ``` root@node:~# mytool This is a test ! value : default_value root@node:~# mytool --your_var=newValue This is a test ! value : newValue root@node:~# mytool localtionValue This is a test ! value : localtionValue ``` **Perfection !** 如果你已經走到這一步的話,其實已經能寫很多簡單的命令列工具了。 為什麼說簡單呢,目前都是使用函式來完成一個個命令的邏輯,多一個子命令多寫一個函式,慢慢的就會讓這個檔案變的龐雜和冗餘。而且久而久之,肯定會出現一些看起來很相似,卻要使用ctrl + c-v大法去完成的事情。甚至有一些邏輯,想要實現還要自己去做更復雜的邏輯。 ------ ### 四、快速進階 此時,一年級的已經可以下課了,二年級的請注意聽講了,下面,我們要講的是: **類的使用** **命令巢狀** **屬性訪問** **鏈式呼叫** #### 4.1 類的使用 通過一個簡單的算數類來了解其用法,在下列用法中,我們在fire中註冊了一個類物件。 ```python import fire class Calculator(object): def add(self, x, y): return x + y def multiply(self, x, y): return x * y if __name__ == '__main__': calculator = Calculator() fire.Fire(calculator) ``` 以下是呼叫測試 ```python $ python example.py add 10 20 30 $ python example.py multiply 10 20 200 ``` 當然我們也可以註冊一個類。 ```python import fire class Calculator(object): def add(self, x, y): return x + y def multiply(self, x, y): return x * y if __name__ == '__main__': fire.Fire(Calculator) ``` 跑一下看看: ``` $ python example.py add 10 20 30 $ python example.py multiply 10 20 200 ``` 就這?當然不會,我們還可以通過引數控制例項屬性,就像下面的例子: ```python import fire class BrokenCalculator(object): def __init__(self, offset=1): self._offset = offset def add(self, x, y): return x + y + self._offset def multiply(self, x, y): return x * y + self._offset if __name__ == '__main__': fire.Fire(BrokenCalculator) ``` 我們可以看到,新增了一個offset的例項屬性,預設值是1. ``` $ python example.py add 10 20 31 $ python example.py multiply 10 20 201 ``` 重點來了,我們可以直接給屬性賦值,以此來增加你命令列工具的自由度。 ``` $ python example.py add 10 20 --offset=0 30 $ python example.py multiply 10 20 --offset=0 200 ``` #### 4.2 命令巢狀 通過不同的類來控制某些同名命令,其實也是將各個命令分門別類更具條理性的管理。可以看到以下用法。 ```python import fire class Sing: def run(self): print('sing sing sing ...') class Dance: def run(self): print('dance dance dance ...') def status(self): print('Around.') class Pipeline: def __init__(self): self.sing = Sing() self.dance = Dance() def run(self): self.sing.run() self.dance.run() self.dance.status() if __name__ == '__main__': fire.Fire(Pipeline) ``` 跑跑看: ```shell # python3 ball.py run sing sing sing ... dance dance dance ... Around. # python3 ball.py sing run sing sing sing ... # python3 ball.py dance run dance dance dance ... # python3 ball.py dance status Around. ``` 根據自定義的一個Pipeline類,我們可以自己組合想要的命令列效果,給子命令再分配不同的子集。 #### 4.3 屬性訪問 其實前面說到類的時候已經簡單的說過屬性訪問(就是那個offset的例子,行啦,忘了就不用往上翻了),這裡再詳細舉例說明一下。 ```shell # python3 test.py --age=6 outinfo Xiao Ming is 6 years old and in the First grade # python3 test.py --age=7 outinfo Xiao Ming is 7 years old and in the Second grade # python3 test.py --age=8 outinfo Xiao Ming is 8 years old and in the Third grade ``` 綜上,我們可以通過控制類的屬性來構造類物件。 嘮到這兒了,再嘮一個騷操作 #### 4.4 鏈式呼叫 官方給的例子不太好看,沒有那麼讓人一眼就看懂的感覺,找了個四則運算的簡單示例: ```python import fire class Calculator: def __init__(self): self.result = 0 self.express = '0' def __str__(self): return f'{self.express} = {self.result}' def add(self, x): self.result += x self.express = f'{self.express}+{x}' return self def sub(self, x): self.result -= x self.express = f'{self.express}-{x}' return self def mul(self, x): self.result *= x self.express = f'({self.express})*{x}' return self def div(self, x): self.result /= x self.express = f'({self.express})/{x}' return self if __name__ == '__main__': fire.Fire(Calculator) ``` 函式名呢,`add`、`sub`、`mul`、`div`分別對應 加、減、乘、除四則運算,每個方法都接受 `x` 引數去運算,返回`self`,這樣不論往後鏈式呼叫多少次都可以,結束呼叫到 `__str__` 打印出結果。 `__str__` 在 `fire` 中用來完成自定義序列化。如果不提供這個方法,在鏈式呼叫完成後將會列印幫助內容。 ```shell # python3 test.py add 2 sub 1.5 mul 3 div 2 ((0+2-1.5)*3)/2 = 0.75 # python3 test.py add 4 sub 2.5 mul 2 div 4 mul 3 sub 5 add 2 (((0+4-2.5)*2)/4)*3-5+2 = -0.75 ``` 看完這個大家應該明白鏈式呼叫的運用了,這個時候再來看一下官方示例也許會輕鬆一些。 ```python import fire class BinaryCanvas(object): """A canvas with which to make binary art, one bit at a time.""" def __init__(self, size=10): self.pixels = [[0] * size for _ in range(size)] self._size = size self._row = 0 # The row of the cursor. self._col = 0 # The column of the cursor. def __str__(self): return '\n'.join(' '.join(str(pixel) for pixel in row) for row in self.pixels) def show(self): print(self) return self def move(self, row, col): self._row = row % self._size self._col = col % self._size return self def on(self): return self.set(1) def off(self): return self.set(0) def set(self, value): self.pixels[self._row][self._col] = value return self if __name__ == '__main__': fire.Fire(BinaryCanvas) ``` 跑一下看看: ```shell $ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ``` PS:我要不說,誰能看出來這是個笑臉??? ------ ### 最後一課 最後看看官方給出的 fire 的內建引數吧,具體怎麼應用大家就自己研究咯。 ##### Flags | Using a CLI | Command | Notes | | ----------- | --------------------------------- | ---------------------------------------- | | Help | `command -- --help` | 顯示命令的幫助和使用資訊。 | | REPL | `command -- --interactive` | 進入互動模式。 | | Separator | `command -- --separator=X` | 這將分隔符設定為' X '。預設分隔符是“-”。 | | Completion | `command -- --completion [shell]` | 為CLI生成一個補全的shell指令碼。 | | Trace | `command -- --trace` | 跟蹤fire命令呼叫後發生了啥子。 | | Verbose | `command -- --verbose` | 在輸出中包含私有成員。 | [fire-GitHub地址](https://github.com/google/pyth